From 864827f788cd1671a4db2ebc159c1c8ab031b7ad Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 23 Nov 2023 18:56:20 +0900 Subject: Hard mute (#12376) * feat(backend,misskey-js): hard mute storage in backend * fix(backend,misskey-js): mute word record type * chore(frontend): generalize XWordMute * feat(frontend): configure hard mute * feat(frontend): hard mute notes on the timelines * lint(backend,frontend): fix lint failure * chore(misskey-js): update api.md * fix(backend): test failure * chore(frontend): check word mute for reply * chore: limit hard mute count --- .../backend/src/server/api/endpoints/i/update.ts | 34 +++++++++++++++++----- 1 file changed, 27 insertions(+), 7 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/i/update.ts') diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index b00aa87bee..8ba29c5658 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -123,6 +123,11 @@ export const meta = { }, } as const; +const muteWords = { type: 'array', items: { oneOf: [ + { type: 'array', items: { type: 'string' } }, + { type: 'string' } +] } } as const; + export const paramDef = { type: 'object', properties: { @@ -171,7 +176,8 @@ export const paramDef = { autoSensitive: { type: 'boolean' }, ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true }, - mutedWords: { type: 'array' }, + mutedWords: muteWords, + hardMutedWords: muteWords, mutedInstances: { type: 'array', items: { type: 'string', } }, @@ -234,16 +240,20 @@ export default class extends Endpoint { // eslint- if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; - if (ps.mutedWords !== undefined) { + + function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) { // TODO: ちゃんと数える const length = JSON.stringify(ps.mutedWords).length; - if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) { + if (length > limit) { throw new ApiError(meta.errors.tooManyMutedWords); } + } + + function validateMuteWordRegex(mutedWords: (string[] | string)[]) { + for (const mutedWord of mutedWords) { + if (typeof mutedWord !== "string") continue; - // validate regular expression syntax - ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => { - const regexp = x.match(/^\/(.+)\/(.*)$/); + const regexp = mutedWord.match(/^\/(.+)\/(.*)$/); if (!regexp) throw new ApiError(meta.errors.invalidRegexp); try { @@ -251,11 +261,21 @@ export default class extends Endpoint { // eslint- } catch (err) { throw new ApiError(meta.errors.invalidRegexp); } - }); + } + } + + if (ps.mutedWords !== undefined) { + checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); + validateMuteWordRegex(ps.mutedWords); profileUpdates.mutedWords = ps.mutedWords; profileUpdates.enableWordMute = ps.mutedWords.length > 0; } + if (ps.hardMutedWords !== undefined) { + checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); + validateMuteWordRegex(ps.hardMutedWords); + profileUpdates.hardMutedWords = ps.hardMutedWords; + } if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig; if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; -- cgit v1.2.3-freya From 536f08c401e82b9d34e2435f62ab2ac96473e77b Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Fri, 24 Nov 2023 15:09:25 +0900 Subject: fix: hard mute limit not applied (#12428) * fix: hard mute limit not applied * Update CHANGELOG.md --- CHANGELOG.md | 1 + packages/backend/src/server/api/endpoints/i/update.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'packages/backend/src/server/api/endpoints/i/update.ts') diff --git a/CHANGELOG.md b/CHANGELOG.md index c49aa0e899..5678da25a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### General - Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed) - Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83) +- Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加 - Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 ### Client diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 8ba29c5658..b045c01189 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -243,7 +243,7 @@ export default class extends Endpoint { // eslint- function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) { // TODO: ちゃんと数える - const length = JSON.stringify(ps.mutedWords).length; + const length = JSON.stringify(mutedWords).length; if (length > limit) { throw new ApiError(meta.errors.tooManyMutedWords); } -- cgit v1.2.3-freya From 5472f4b934c8ca8c702152a4a927b4ac94cf3fdb Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 13 Dec 2023 16:56:19 +0900 Subject: enhance: アイコンデコレーションを複数設定できるように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + locales/index.d.ts | 5 ++ locales/ja-JP.yml | 5 ++ packages/backend/src/core/RoleService.ts | 3 ++ packages/backend/src/models/json-schema/role.ts | 1 + packages/backend/src/models/json-schema/user.ts | 4 ++ .../backend/src/server/api/endpoints/i/update.ts | 10 ++-- packages/frontend/src/account.ts | 2 +- .../frontend/src/components/MkDrive.folder.vue | 9 ++-- packages/frontend/src/components/MkWindow.vue | 2 +- packages/frontend/src/components/global/MkA.vue | 2 +- .../frontend/src/components/global/MkAvatar.vue | 53 ++++++++-------------- packages/frontend/src/const.ts | 1 + packages/frontend/src/pages/admin/roles.editor.vue | 22 ++++++++- packages/frontend/src/pages/admin/roles.vue | 7 +++ .../settings/profile.avatar-decoration-dialog.vue | 11 +++-- packages/frontend/src/pages/settings/profile.vue | 39 ++++++++++++---- 17 files changed, 115 insertions(+), 62 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/i/update.ts') diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d1434cde4..e5ff09edec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed) - Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83) - Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加 +- Enhance: アイコンデコレーションを複数設定できるように - Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 ### Client diff --git a/locales/index.d.ts b/locales/index.d.ts index 846a6d503d..d32023f5ac 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -264,6 +264,7 @@ export interface Locale { "removeAreYouSure": string; "deleteAreYouSure": string; "resetAreYouSure": string; + "areYouSure": string; "saved": string; "messaging": string; "upload": string; @@ -1160,6 +1161,7 @@ export interface Locale { "avatarDecorations": string; "attach": string; "detach": string; + "detachAll": string; "angle": string; "flip": string; "showAvatarDecorations": string; @@ -1173,6 +1175,7 @@ export interface Locale { "doReaction": string; "code": string; "reloadRequiredToApplySettings": string; + "remainingN": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; @@ -1701,6 +1704,7 @@ export interface Locale { "canHideAds": string; "canSearchNotes": string; "canUseTranslator": string; + "avatarDecorationLimit": string; }; "_condition": { "isLocal": string; @@ -2181,6 +2185,7 @@ export interface Locale { "changeAvatar": string; "changeBanner": string; "verifiedLinkDescription": string; + "avatarDecorationMax": string; }; "_exportOrImport": { "allNotes": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0d84440bc8..2ac57fd311 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -261,6 +261,7 @@ removed: "削除しました" removeAreYouSure: "「{x}」を削除しますか?" deleteAreYouSure: "「{x}」を削除しますか?" resetAreYouSure: "リセットしますか?" +areYouSure: "よろしいですか?" saved: "保存しました" messaging: "チャット" upload: "アップロード" @@ -1157,6 +1158,7 @@ tosAndPrivacyPolicy: "利用規約・プライバシーポリシー" avatarDecorations: "アイコンデコレーション" attach: "付ける" detach: "外す" +detachAll: "全て外す" angle: "角度" flip: "反転" showAvatarDecorations: "アイコンのデコレーションを表示" @@ -1170,6 +1172,7 @@ cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述 doReaction: "リアクションする" code: "コード" reloadRequiredToApplySettings: "設定の反映にはリロードが必要です。" +remainingN: "残り: {n}" _announcement: forExistingUsers: "既存ユーザーのみ" @@ -1610,6 +1613,7 @@ _role: canHideAds: "広告の非表示" canSearchNotes: "ノート検索の利用" canUseTranslator: "翻訳機能の利用" + avatarDecorationLimit: "アイコンデコレーションの最大取付個数" _condition: isLocal: "ローカルユーザー" isRemote: "リモートユーザー" @@ -2084,6 +2088,7 @@ _profile: changeAvatar: "アイコン画像を変更" changeBanner: "バナー画像を変更" verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。" + avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。" _exportOrImport: allNotes: "全てのノート" diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 29e48aa8ca..4de719d6a0 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -47,6 +47,7 @@ export type RolePolicies = { userListLimit: number; userEachUserListsLimit: number; rateLimitFactor: number; + avatarDecorationLimit: number; }; export const DEFAULT_POLICIES: RolePolicies = { @@ -73,6 +74,7 @@ export const DEFAULT_POLICIES: RolePolicies = { userListLimit: 10, userEachUserListsLimit: 50, rateLimitFactor: 1, + avatarDecorationLimit: 1, }; @Injectable() @@ -326,6 +328,7 @@ export class RoleService implements OnApplicationShutdown { userListLimit: calc('userListLimit', vs => Math.max(...vs)), userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)), rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)), + avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)), }; } diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index dd2f32b14d..b0c6804bb8 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -145,6 +145,7 @@ export const packedRoleSchema = { userEachUserListsLimit: rolePolicyValue, canManageAvatarDecorations: rolePolicyValue, canUseTranslator: rolePolicyValue, + avatarDecorationLimit: rolePolicyValue, }, }, usersCount: { diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index c6b2707b80..c6b96b85f0 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -672,6 +672,10 @@ export const packedMeDetailedOnlySchema = { type: 'number', nullable: false, optional: false, }, + avatarDecorationLimit: { + type: 'number', + nullable: false, optional: false, + }, }, }, //#region secrets diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index b045c01189..399e6b88cb 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -125,7 +125,7 @@ export const meta = { const muteWords = { type: 'array', items: { oneOf: [ { type: 'array', items: { type: 'string' } }, - { type: 'string' } + { type: 'string' }, ] } } as const; export const paramDef = { @@ -137,7 +137,7 @@ export const paramDef = { birthday: { ...birthdaySchema, nullable: true }, lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true }, avatarId: { type: 'string', format: 'misskey:id', nullable: true }, - avatarDecorations: { type: 'array', maxItems: 1, items: { + avatarDecorations: { type: 'array', maxItems: 16, items: { type: 'object', properties: { id: { type: 'string', format: 'misskey:id' }, @@ -251,7 +251,7 @@ export default class extends Endpoint { // eslint- function validateMuteWordRegex(mutedWords: (string[] | string)[]) { for (const mutedWord of mutedWords) { - if (typeof mutedWord !== "string") continue; + if (typeof mutedWord !== 'string') continue; const regexp = mutedWord.match(/^\/(.+)\/(.*)$/); if (!regexp) throw new ApiError(meta.errors.invalidRegexp); @@ -329,12 +329,14 @@ export default class extends Endpoint { // eslint- if (ps.avatarDecorations) { const decorations = await this.avatarDecorationService.getAll(true); - const myRoles = await this.roleService.getUserRoles(user.id); + const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]); const allRoles = await this.roleService.getRoles(); const decorationIds = decorations .filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))) .map(d => d.id); + if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); + updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({ id: d.id, angle: d.angle ?? 0, diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index 0e4e4b50ff..a6af298024 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -284,7 +284,7 @@ export async function openAccountMenu(opts: { text: i18n.ts.profile, to: `/@${ $i.username }`, avatar: $i, - }, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { + }, { type: 'divider' }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { type: 'parent' as const, icon: 'ti ti-plus', text: i18n.ts.addAccount, diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 5322664664..b0c14d1f0b 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -39,6 +39,7 @@ import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { claimAchievement } from '@/scripts/achievements.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { MenuItem } from '@/types/menu.js'; const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder; @@ -250,7 +251,7 @@ function setAsUploadFolder() { } function onContextmenu(ev: MouseEvent) { - let menu; + let menu: MenuItem[]; menu = [{ text: i18n.ts.openInWindow, icon: 'ti ti-app-window', @@ -260,18 +261,18 @@ function onContextmenu(ev: MouseEvent) { }, { }, 'closed'); }, - }, null, { + }, { type: 'divider' }, { text: i18n.ts.rename, icon: 'ti ti-forms', action: rename, - }, null, { + }, { type: 'divider' }, { text: i18n.ts.delete, icon: 'ti ti-trash', danger: true, action: deleteFolder, }]; if (defaultStore.state.devMode) { - menu = menu.concat([null, { + menu = menu.concat([{ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyFolderId, action: () => { diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue index 1150a29e03..7c8ffcccf9 100644 --- a/packages/frontend/src/components/MkWindow.vue +++ b/packages/frontend/src/components/MkWindow.vue @@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue'; import contains from '@/scripts/contains.js'; import * as os from '@/os.js'; -import { MenuItem } from '@/types/menu'; +import { MenuItem } from '@/types/menu.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue index 809dae421a..5552e96ee0 100644 --- a/packages/frontend/src/components/global/MkA.vue +++ b/packages/frontend/src/components/global/MkA.vue @@ -57,7 +57,7 @@ function onContextmenu(ev) { action: () => { router.push(props.to, 'forcePage'); }, - }, null, { + }, { type: 'divider' }, { icon: 'ti ti-external-link', text: i18n.ts.openInNewTab, action: () => { diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index c7e50e275a..6aa9a42037 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -23,16 +23,18 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -57,19 +59,14 @@ const props = withDefaults(defineProps<{ link?: boolean; preview?: boolean; indicator?: boolean; - decoration?: { - url: string; - angle?: number; - flipH?: boolean; - flipV?: boolean; - }; + decorations?: Misskey.entities.UserDetailed['avatarDecorations'][number][]; forceShowDecoration?: boolean; }>(), { target: null, link: false, preview: false, indicator: false, - decoration: undefined, + decorations: undefined, forceShowDecoration: false, }); @@ -92,27 +89,13 @@ function onClick(ev: MouseEvent): void { emit('click', ev); } -function getDecorationAngle() { - let angle; - if (props.decoration) { - angle = props.decoration.angle ?? 0; - } else if (props.user.avatarDecorations.length > 0) { - angle = props.user.avatarDecorations[0].angle ?? 0; - } else { - angle = 0; - } +function getDecorationAngle(decoration: Misskey.entities.UserDetailed['avatarDecorations'][number]) { + const angle = decoration.angle ?? 0; return angle === 0 ? undefined : `${angle * 360}deg`; } -function getDecorationScale() { - let scaleX; - if (props.decoration) { - scaleX = props.decoration.flipH ? -1 : 1; - } else if (props.user.avatarDecorations.length > 0) { - scaleX = props.user.avatarDecorations[0].flipH ? -1 : 1; - } else { - scaleX = 1; - } +function getDecorationScale(decoration: Misskey.entities.UserDetailed['avatarDecorations'][number]) { + const scaleX = decoration.flipH ? -1 : 1; return scaleX === 1 ? undefined : `${scaleX} 1`; } diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index 397f804822..f016b7aa02 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -81,6 +81,7 @@ export const ROLE_POLICIES = [ 'userListLimit', 'userEachUserListsLimit', 'rateLimitFactor', + 'avatarDecorationLimit', ] as const; // なんか動かない diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index a8e0e8bbd1..5ded8d6931 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -531,6 +531,26 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + +
+ + + + + + + + + +
+
@@ -549,7 +569,7 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkRange from '@/components/MkRange.vue'; import FormSlot from '@/components/form/slot.vue'; import { i18n } from '@/i18n.js'; -import { ROLE_POLICIES } from '@/const'; +import { ROLE_POLICIES } from '@/const.js'; import { instance } from '@/instance.js'; import { deepClone } from '@/scripts/clone.js'; diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index db4595b150..1bb91a0a5b 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -192,6 +192,13 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + + {{ i18n.ts.save }} diff --git a/packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue b/packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue index 4d571bc9ba..c27a21217b 100644 --- a/packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue +++ b/packages/frontend/src/pages/settings/profile.avatar-decoration-dialog.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ decoration.name }}
- +
@@ -54,6 +54,7 @@ const props = defineProps<{ decoration: { id: string; url: string; + name: string; } }>(); @@ -77,18 +78,18 @@ async function attach() { flipH: flipH.value, }; await os.apiWithDialog('i/update', { - avatarDecorations: [decoration], + avatarDecorations: [...$i.avatarDecorations, decoration], }); - $i.avatarDecorations = [decoration]; + $i.avatarDecorations = [...$i.avatarDecorations, decoration]; dialog.value.close(); } async function detach() { await os.apiWithDialog('i/update', { - avatarDecorations: [], + avatarDecorations: $i.avatarDecorations.filter(x => x.id !== props.decoration.id), }); - $i.avatarDecorations = []; + $i.avatarDecorations = $i.avatarDecorations.filter(x => x.id !== props.decoration.id); dialog.value.close(); } diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index ba75b539e1..a5d3835b93 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -87,16 +87,22 @@ SPDX-License-Identifier: AGPL-3.0-only -
-
-
{{ avatarDecoration.name }}
- - +
+ {{ i18n.t('_profile.avatarDecorationMax', { max: $i?.policies.avatarDecorationLimit }) }} ({{ i18n.t('remainingN', { n: $i?.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }}) + + {{ i18n.ts.detachAll }} + +
+
+
{{ avatarDecoration.name }}
+ + +
@@ -273,6 +279,19 @@ function openDecoration(avatarDecoration) { }, {}, 'closed'); } +function detachAllDecorations() { + os.confirm({ + type: 'warning', + text: i18n.ts.areYouSure, + }).then(async ({ canceled }) => { + if (canceled) return; + await os.apiWithDialog('i/update', { + avatarDecorations: [], + }); + $i.avatarDecorations = []; + }); +} + const headerActions = computed(() => []); const headerTabs = computed(() => []); -- cgit v1.2.3-freya From 417852779f871197f6aec2fec0fd56a2380836e4 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 14 Dec 2023 20:58:08 +0900 Subject: enhance: アイコンデコレーションの位置を微調整できるように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + .../backend/src/core/entities/UserEntityService.ts | 2 ++ packages/backend/src/models/User.ts | 6 ++++-- packages/backend/src/server/api/endpoints/i/update.ts | 4 ++++ packages/frontend/src/components/global/MkAvatar.vue | 7 +++++++ .../settings/profile.avatar-decoration.decoration.vue | 4 +++- .../settings/profile.avatar-decoration.dialog.vue | 18 ++++++++++++++++++ .../src/pages/settings/profile.avatar-decoration.vue | 6 ++++++ 8 files changed, 45 insertions(+), 3 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/i/update.ts') diff --git a/CHANGELOG.md b/CHANGELOG.md index 0581466d7c..599bd463fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ - Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83) - Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加 - Enhance: アイコンデコレーションを複数設定できるように +- Enhance: アイコンデコレーションの位置を微調整できるように - Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 ### Client diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 917f4e06d0..fb7aa0c244 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -362,6 +362,8 @@ export class UserEntityService implements OnModuleInit { id: ud.id, angle: ud.angle || undefined, flipH: ud.flipH || undefined, + offsetX: ud.offsetX || undefined, + offsetY: ud.offsetY || undefined, url: decorations.find(d => d.id === ud.id)!.url, }))) : [], isBot: user.isBot, diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index c3762fcd3e..219497a125 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -143,8 +143,10 @@ export class MiUser { }) public avatarDecorations: { id: string; - angle: number; - flipH: boolean; + angle?: number; + flipH?: boolean; + offsetX?: number; + offsetY?: number; }[]; @Index() diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 399e6b88cb..a56f50115b 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -143,6 +143,8 @@ export const paramDef = { id: { type: 'string', format: 'misskey:id' }, angle: { type: 'number', nullable: true, maximum: 0.5, minimum: -0.5 }, flipH: { type: 'boolean', nullable: true }, + offsetX: { type: 'number', nullable: true, maximum: 0.25, minimum: -0.25 }, + offsetY: { type: 'number', nullable: true, maximum: 0.25, minimum: -0.25 }, }, required: ['id'], } }, @@ -341,6 +343,8 @@ export default class extends Endpoint { // eslint- id: d.id, angle: d.angle ?? 0, flipH: d.flipH ?? false, + offsetX: d.offsetX ?? 0, + offsetY: d.offsetY ?? 0, })); } diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index 9d13c03290..af5b6e44f5 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -31,6 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only :style="{ rotate: getDecorationAngle(decoration), scale: getDecorationScale(decoration), + translate: getDecorationOffset(decoration), }" alt="" > @@ -99,6 +100,12 @@ function getDecorationScale(decoration: Omit) { + const offsetX = decoration.offsetX ?? 0; + const offsetY = decoration.offsetY ?? 0; + return offsetX === 0 && offsetY === 0 ? undefined : `${offsetX * 100}% ${offsetY * 100}%`; +} + const color = ref(); watch(() => props.user.avatarBlurhash, () => { diff --git a/packages/frontend/src/pages/settings/profile.avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/profile.avatar-decoration.decoration.vue index c113868238..9c95b5547e 100644 --- a/packages/frontend/src/pages/settings/profile.avatar-decoration.decoration.vue +++ b/packages/frontend/src/pages/settings/profile.avatar-decoration.decoration.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only @click="emit('click')" >
{{ decoration.name }}
- +
@@ -28,6 +28,8 @@ const props = defineProps<{ }; angle?: number; flipH?: boolean; + offsetX?: number; + offsetY?: number; }>(); const emit = defineEmits<{ diff --git a/packages/frontend/src/pages/settings/profile.avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/profile.avatar-decoration.dialog.vue index 26cacf3c37..77e6b28fad 100644 --- a/packages/frontend/src/pages/settings/profile.avatar-decoration.dialog.vue +++ b/packages/frontend/src/pages/settings/profile.avatar-decoration.dialog.vue @@ -23,6 +23,12 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + @@ -64,10 +70,14 @@ const emit = defineEmits<{ (ev: 'attach', payload: { angle: number; flipH: boolean; + offsetX: number; + offsetY: number; }): void; (ev: 'update', payload: { angle: number; flipH: boolean; + offsetX: number; + offsetY: number; }): void; (ev: 'detach'): void; }>(); @@ -76,6 +86,8 @@ const dialog = shallowRef>(); const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0); const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0); const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false); +const offsetX = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetX : null) ?? 0); +const offsetY = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetY : null) ?? 0); const decorationsForPreview = computed(() => { const decoration = { @@ -83,6 +95,8 @@ const decorationsForPreview = computed(() => { url: props.decoration.url, angle: angle.value, flipH: flipH.value, + offsetX: offsetX.value, + offsetY: offsetY.value, }; const decorations = [...$i.avatarDecorations]; if (props.usingIndex != null) { @@ -101,6 +115,8 @@ async function update() { emit('update', { angle: angle.value, flipH: flipH.value, + offsetX: offsetX.value, + offsetY: offsetY.value, }); dialog.value.close(); } @@ -109,6 +125,8 @@ async function attach() { emit('attach', { angle: angle.value, flipH: flipH.value, + offsetX: offsetX.value, + offsetY: offsetY.value, }); dialog.value.close(); } diff --git a/packages/frontend/src/pages/settings/profile.avatar-decoration.vue b/packages/frontend/src/pages/settings/profile.avatar-decoration.vue index bfef6e0325..8579acfed8 100644 --- a/packages/frontend/src/pages/settings/profile.avatar-decoration.vue +++ b/packages/frontend/src/pages/settings/profile.avatar-decoration.vue @@ -16,6 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-only :decoration="avatarDecorations.find(d => d.id === avatarDecoration.id)" :angle="avatarDecoration.angle" :flipH="avatarDecoration.flipH" + :offsetX="avatarDecoration.offsetX" + :offsetY="avatarDecoration.offsetY" :active="true" @click="openDecoration(avatarDecoration, i)" /> @@ -66,6 +68,8 @@ function openDecoration(avatarDecoration, index?: number) { id: avatarDecoration.id, angle: payload.angle, flipH: payload.flipH, + offsetX: payload.offsetX, + offsetY: payload.offsetY, }; const update = [...$i.avatarDecorations, decoration]; await os.apiWithDialog('i/update', { @@ -78,6 +82,8 @@ function openDecoration(avatarDecoration, index?: number) { id: avatarDecoration.id, angle: payload.angle, flipH: payload.flipH, + offsetX: payload.offsetX, + offsetY: payload.offsetY, }; const update = [...$i.avatarDecorations]; update[index] = decoration; -- cgit v1.2.3-freya From 4e2d8029678951ca3b8b9b40e62901b0c67618ed Mon Sep 17 00:00:00 2001 From: zawa-ch Date: Mon, 18 Dec 2023 20:59:20 +0900 Subject: enhance: “つながりの公開範囲”がフォロー・フォロワー個別設定できるように (#12702) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enhance: “つながりの公開範囲”がフォロー・フォロワー個別設定できるように (#12072) * refactor: crowdin 編集部分のコミットを打ち消し https://github.com/misskey-dev/misskey/pull/12702#issuecomment-1859417158 * refactor: オブジェクトの名前修正 https://github.com/misskey-dev/misskey/pull/12702#issuecomment-1859417158 * fix: 設定項目の説明を削除 名称が具体的になって必要なくなったため https://github.com/misskey-dev/misskey/pull/12702#discussion_r1429932463 --- CHANGELOG.md | 1 + locales/index.d.ts | 4 +- locales/ja-JP.yml | 4 +- .../migration/1702718871541-ffVisibility.js | 35 ++ .../backend/src/core/entities/UserEntityService.ts | 11 +- packages/backend/src/models/UserProfile.ts | 12 +- packages/backend/src/models/json-schema/user.ts | 7 +- .../backend/src/server/ActivityPubServerService.ts | 8 +- .../backend/src/server/api/endpoints/i/update.ts | 6 +- .../src/server/api/endpoints/users/followers.ts | 4 +- .../src/server/api/endpoints/users/following.ts | 4 +- packages/backend/src/server/web/FeedService.ts | 2 +- packages/backend/src/types.ts | 3 +- packages/backend/test/e2e/ff-visibility.ts | 543 ++++++++++++++++++++- packages/backend/test/e2e/users.ts | 15 +- packages/frontend/.storybook/fakes.ts | 3 +- packages/frontend/src/components/MkUserInfo.vue | 6 +- packages/frontend/src/components/MkUserPopup.vue | 6 +- packages/frontend/src/pages/settings/privacy.vue | 18 +- packages/frontend/src/pages/user/home.vue | 6 +- packages/frontend/src/scripts/isFfVisibleForMe.ts | 14 +- packages/misskey-js/src/consts.ts | 4 +- packages/misskey-js/src/index.ts | 3 +- 23 files changed, 648 insertions(+), 71 deletions(-) create mode 100644 packages/backend/migration/1702718871541-ffVisibility.js (limited to 'packages/backend/src/server/api/endpoints/i/update.ts') diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a99a6ca2c..dd8c492782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - Feat: TL上からノートが見えなくなるワードミュートであるハードミュートを追加 - Enhance: アイコンデコレーションを複数設定できるように - Enhance: アイコンデコレーションの位置を微調整できるように +- Enhance: つながりの公開範囲をフォロー/フォロワーで個別に設定可能に #12072 - Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 ### Client diff --git a/locales/index.d.ts b/locales/index.d.ts index cd15bd968f..25a16d4a4d 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -884,8 +884,8 @@ export interface Locale { "classic": string; "muteThread": string; "unmuteThread": string; - "ffVisibility": string; - "ffVisibilityDescription": string; + "followingVisibility": string; + "followersVisibility": string; "continueThread": string; "deleteAccountConfirm": string; "incorrectPassword": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 5537db9d56..308b7ae67d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -881,8 +881,8 @@ makeReactionsPublicDescription: "あなたがしたリアクション一覧を classic: "クラシック" muteThread: "スレッドをミュート" unmuteThread: "スレッドのミュートを解除" -ffVisibility: "つながりの公開範囲" -ffVisibilityDescription: "自分のフォロー/フォロワー情報の公開範囲を設定できます。" +followingVisibility: "フォローの公開範囲" +followersVisibility: "フォロワーの公開範囲" continueThread: "さらにスレッドを見る" deleteAccountConfirm: "アカウントが削除されます。よろしいですか?" incorrectPassword: "パスワードが間違っています。" diff --git a/packages/backend/migration/1702718871541-ffVisibility.js b/packages/backend/migration/1702718871541-ffVisibility.js new file mode 100644 index 0000000000..24b1873134 --- /dev/null +++ b/packages/backend/migration/1702718871541-ffVisibility.js @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ffVisibility1702718871541 { + constructor() { + this.name = 'ffVisibility1702718871541'; + } + async up(queryRunner) { + await queryRunner.query(`CREATE TYPE "public"."user_profile_followingvisibility_enum" AS ENUM('public', 'followers', 'private')`); + await queryRunner.query(`CREATE CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followingvisibility_enum") WITH INOUT AS ASSIGNMENT`); + await queryRunner.query(`CREATE TYPE "public"."user_profile_followersVisibility_enum" AS ENUM('public', 'followers', 'private')`); + await queryRunner.query(`CREATE CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followersVisibility_enum") WITH INOUT AS ASSIGNMENT`); + await queryRunner.query(`ALTER TABLE "user_profile" ADD "followingVisibility" "public"."user_profile_followingvisibility_enum" NOT NULL DEFAULT 'public'`); + await queryRunner.query(`ALTER TABLE "user_profile" ADD "followersVisibility" "public"."user_profile_followersVisibility_enum" NOT NULL DEFAULT 'public'`); + await queryRunner.query(`UPDATE "user_profile" SET "followingVisibility" = "ffVisibility"`); + await queryRunner.query(`UPDATE "user_profile" SET "followersVisibility" = "ffVisibility"`); + await queryRunner.query(`DROP CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followersVisibility_enum")`); + await queryRunner.query(`DROP CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followingvisibility_enum")`); + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "ffVisibility"`); + await queryRunner.query(`DROP TYPE "public"."user_profile_ffvisibility_enum"`); + } + async down(queryRunner) { + await queryRunner.query(`CREATE TYPE "public"."user_profile_ffvisibility_enum" AS ENUM('public', 'followers', 'private')`); + await queryRunner.query(`ALTER TABLE "user_profile" ADD "ffVisibility" "public"."user_profile_ffvisibility_enum" NOT NULL DEFAULT 'public'`); + await queryRunner.query(`CREATE CAST ("public"."user_profile_followingvisibility_enum" AS "public"."user_profile_ffvisibility_enum") WITH INOUT AS ASSIGNMENT`); + await queryRunner.query(`UPDATE "user_profile" SET ffVisibility = "user_profile"."followingVisibility"`); + await queryRunner.query(`DROP CAST ("public"."user_profile_followingvisibility_enum" AS "public"."user_profile_ffvisibility_enum")`); + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "followersVisibility"`); + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "followingVisibility"`); + await queryRunner.query(`DROP TYPE "public"."user_profile_followersVisibility_enum"`); + await queryRunner.query(`DROP TYPE "public"."user_profile_followingvisibility_enum"`); + } +} diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index fb7aa0c244..ef815a388a 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -332,13 +332,13 @@ export class UserEntityService implements OnModuleInit { const profile = opts.detail ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null; const followingCount = profile == null ? null : - (profile.ffVisibility === 'public') || isMe ? user.followingCount : - (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount : + (profile.followingVisibility === 'public') || isMe ? user.followingCount : + (profile.followingVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount : null; const followersCount = profile == null ? null : - (profile.ffVisibility === 'public') || isMe ? user.followersCount : - (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : + (profile.followersVisibility === 'public') || isMe ? user.followersCount : + (profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : null; const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null; @@ -417,7 +417,8 @@ export class UserEntityService implements OnModuleInit { pinnedPageId: profile!.pinnedPageId, pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null, publicReactions: profile!.publicReactions, - ffVisibility: profile!.ffVisibility, + followersVisibility: profile!.followersVisibility, + followingVisibility: profile!.followingVisibility, twoFactorEnabled: profile!.twoFactorEnabled, usePasswordLessLogin: profile!.usePasswordLessLogin, securityKeys: profile!.twoFactorEnabled diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 6659a01412..328dbeaa1c 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -4,7 +4,7 @@ */ import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; -import { obsoleteNotificationTypes, ffVisibility, notificationTypes } from '@/types.js'; +import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js'; import { id } from './util/id.js'; import { MiUser } from './User.js'; import { MiPage } from './Page.js'; @@ -94,10 +94,16 @@ export class MiUserProfile { public publicReactions: boolean; @Column('enum', { - enum: ffVisibility, + enum: followingVisibilities, default: 'public', }) - public ffVisibility: typeof ffVisibility[number]; + public followingVisibility: typeof followingVisibilities[number]; + + @Column('enum', { + enum: followersVisibilities, + default: 'public', + }) + public followersVisibility: typeof followersVisibilities[number]; @Column('varchar', { length: 128, nullable: true, diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 7a3ca58269..1b86b1bf10 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -311,7 +311,12 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'boolean', nullable: false, optional: false, }, - ffVisibility: { + followingVisibility: { + type: 'string', + nullable: false, optional: false, + enum: ['public', 'followers', 'private'], + }, + followersVisibility: { type: 'string', nullable: false, optional: false, enum: ['public', 'followers', 'private'], diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 2bc7e1136a..68e426b5bc 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -195,11 +195,11 @@ export class ActivityPubServerService { //#region Check ff visibility const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.ffVisibility === 'private') { + if (profile.followersVisibility === 'private') { reply.code(403); reply.header('Cache-Control', 'public, max-age=30'); return; - } else if (profile.ffVisibility === 'followers') { + } else if (profile.followersVisibility === 'followers') { reply.code(403); reply.header('Cache-Control', 'public, max-age=30'); return; @@ -287,11 +287,11 @@ export class ActivityPubServerService { //#region Check ff visibility const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.ffVisibility === 'private') { + if (profile.followingVisibility === 'private') { reply.code(403); reply.header('Cache-Control', 'public, max-age=30'); return; - } else if (profile.ffVisibility === 'followers') { + } else if (profile.followingVisibility === 'followers') { reply.code(403); reply.header('Cache-Control', 'public, max-age=30'); return; diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index a56f50115b..eed3082258 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -176,7 +176,8 @@ export const paramDef = { receiveAnnouncementEmail: { type: 'boolean' }, alwaysMarkNsfw: { type: 'boolean' }, autoSensitive: { type: 'boolean' }, - ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, + followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, + followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true }, mutedWords: muteWords, hardMutedWords: muteWords, @@ -241,7 +242,8 @@ export default class extends Endpoint { // eslint- if (ps.lang !== undefined) profileUpdates.lang = ps.lang; if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; - if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; + if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility; + if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility; function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) { // TODO: ちゃんと数える diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index b22fd2ff7a..5706e46b96 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -93,11 +93,11 @@ export default class extends Endpoint { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.ffVisibility === 'private') { + if (profile.followersVisibility === 'private') { if (me == null || (me.id !== user.id)) { throw new ApiError(meta.errors.forbidden); } - } else if (profile.ffVisibility === 'followers') { + } else if (profile.followersVisibility === 'followers') { if (me == null) { throw new ApiError(meta.errors.forbidden); } else if (me.id !== user.id) { diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index ead7ba8c40..794fb04f10 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -101,11 +101,11 @@ export default class extends Endpoint { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.ffVisibility === 'private') { + if (profile.followingVisibility === 'private') { if (me == null || (me.id !== user.id)) { throw new ApiError(meta.errors.forbidden); } - } else if (profile.ffVisibility === 'followers') { + } else if (profile.followingVisibility === 'followers') { if (me == null) { throw new ApiError(meta.errors.forbidden); } else if (me.id !== user.id) { diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts index dd4304e6ef..dfda85aac9 100644 --- a/packages/backend/src/server/web/FeedService.ts +++ b/packages/backend/src/server/web/FeedService.ts @@ -60,7 +60,7 @@ export class FeedService { title: `${author.name} (@${user.username}@${this.config.host})`, updated: notes.length !== 0 ? this.idService.parse(notes[0].id).date : undefined, generator: 'Misskey', - description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`, + description: `${user.notesCount} Notes, ${profile.followingVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.followersVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`, link: author.link, image: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), feedLinks: { diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 1fb3d6a6ce..e085407de0 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -25,7 +25,8 @@ export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; -export const ffVisibility = ['public', 'followers', 'private'] as const; +export const followingVisibilities = ['public', 'followers', 'private'] as const; +export const followersVisibilities = ['public', 'followers', 'private'] as const; export const moderationLogTypes = [ 'updateServerSettings', diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts index 7841e057bf..1fbd45c741 100644 --- a/packages/backend/test/e2e/ff-visibility.ts +++ b/packages/backend/test/e2e/ff-visibility.ts @@ -26,9 +26,10 @@ describe('FF visibility', () => { await app.close(); }); - test('ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { + test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { await api('/i/update', { - ffVisibility: 'public', + followingVisibility: 'public', + followersVisibility: 'public', }, alice); const followingRes = await api('/users/following', { @@ -44,9 +45,88 @@ describe('FF visibility', () => { assert.strictEqual(Array.isArray(followersRes.body), true); }); - test('ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる', async () => { + test('followingVisibility が public であれば followersVisibility の設定に関わらずユーザーのフォローを誰でも見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'public', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + }); + + test('followersVisibility が public であれば followingVisibility の設定に関わらずユーザーのフォロワーを誰でも見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'public', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'public', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'public', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + }); + + test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを自分で見れる', async () => { await api('/i/update', { - ffVisibility: 'followers', + followingVisibility: 'followers', + followersVisibility: 'followers', }, alice); const followingRes = await api('/users/following', { @@ -62,9 +142,88 @@ describe('FF visibility', () => { assert.strictEqual(Array.isArray(followersRes.body), true); }); - test('ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => { + test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'public', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + }); + + test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + }); + + test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーを非フォロワーが見れない', async () => { await api('/i/update', { - ffVisibility: 'followers', + followingVisibility: 'followers', + followersVisibility: 'followers', }, alice); const followingRes = await api('/users/following', { @@ -78,9 +237,82 @@ describe('FF visibility', () => { assert.strictEqual(followersRes.status, 400); }); - test('ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => { + test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらず非フォロワーが見れない', async () => { + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'public', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + }); + + test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらず非フォロワーが見れない', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'followers', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + }); + + test('followingVisibility, followersVisibility がともに followers なユーザーのフォロー/フォロワーをフォロワーが見れる', async () => { await api('/i/update', { - ffVisibility: 'followers', + followingVisibility: 'followers', + followersVisibility: 'followers', }, alice); await api('/following/create', { @@ -100,9 +332,106 @@ describe('FF visibility', () => { assert.strictEqual(Array.isArray(followersRes.body), true); }); - test('ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる', async () => { + test('followingVisibility が followers なユーザーのフォローを followersVisibility の設定に関わらずフォロワーが見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'public', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'private', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + }); + + test('followersVisibility が followers なユーザーのフォロワーを followingVisibility の設定に関わらずフォロワーが見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'followers', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'followers', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'followers', + }, alice); + await api('/following/create', { + userId: alice.id, + }, bob); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + }); + + test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを自分で見れる', async () => { await api('/i/update', { - ffVisibility: 'private', + followingVisibility: 'private', + followersVisibility: 'private', }, alice); const followingRes = await api('/users/following', { @@ -118,9 +447,88 @@ describe('FF visibility', () => { assert.strictEqual(Array.isArray(followersRes.body), true); }); - test('ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない', async () => { + test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず自分で見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'public', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, alice); + assert.strictEqual(followingRes.status, 200); + assert.strictEqual(Array.isArray(followingRes.body), true); + } + }); + + test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず自分で見れる', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, alice); + assert.strictEqual(followersRes.status, 200); + assert.strictEqual(Array.isArray(followersRes.body), true); + } + }); + + test('followingVisibility, followersVisibility がともに private なユーザーのフォロー/フォロワーを他人が見れない', async () => { await api('/i/update', { - ffVisibility: 'private', + followingVisibility: 'private', + followersVisibility: 'private', }, alice); const followingRes = await api('/users/following', { @@ -134,36 +542,129 @@ describe('FF visibility', () => { assert.strictEqual(followersRes.status, 400); }); + test('followingVisibility が private なユーザーのフォローを followersVisibility の設定に関わらず他人が見れない', async () => { + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'public', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'followers', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'private', + }, alice); + + const followingRes = await api('/users/following', { + userId: alice.id, + }, bob); + assert.strictEqual(followingRes.status, 400); + } + }); + + test('followersVisibility が private なユーザーのフォロワーを followingVisibility の設定に関わらず他人が見れない', async () => { + { + await api('/i/update', { + followingVisibility: 'public', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'followers', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + { + await api('/i/update', { + followingVisibility: 'private', + followersVisibility: 'private', + }, alice); + + const followersRes = await api('/users/followers', { + userId: alice.id, + }, bob); + assert.strictEqual(followersRes.status, 400); + } + }); + describe('AP', () => { - test('ffVisibility が public 以外ならばAPからは取得できない', async () => { + test('followingVisibility が public 以外ならばAPからはフォローを取得できない', async () => { { await api('/i/update', { - ffVisibility: 'public', + followingVisibility: 'public', }, alice); const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); assert.strictEqual(followingRes.status, 200); - assert.strictEqual(followersRes.status, 200); } { await api('/i/update', { - ffVisibility: 'followers', + followingVisibility: 'followers', }, alice); const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); assert.strictEqual(followingRes.status, 403); - assert.strictEqual(followersRes.status, 403); } { await api('/i/update', { - ffVisibility: 'private', + followingVisibility: 'private', }, alice); const followingRes = await simpleGet(`/users/${alice.id}/following`, 'application/activity+json'); - const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); assert.strictEqual(followingRes.status, 403); + } + }); + + test('followersVisibility が public 以外ならばAPからはフォロワーを取得できない', async () => { + { + await api('/i/update', { + followersVisibility: 'public', + }, alice); + + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); + assert.strictEqual(followersRes.status, 200); + } + { + await api('/i/update', { + followersVisibility: 'followers', + }, alice); + + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); + assert.strictEqual(followersRes.status, 403); + } + { + await api('/i/update', { + followersVisibility: 'private', + }, alice); + + const followersRes = await simpleGet(`/users/${alice.id}/followers`, 'application/activity+json'); assert.strictEqual(followersRes.status, 403); } }); diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 2ce8fbc129..9c4cbac368 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -112,7 +112,8 @@ describe('ユーザー', () => { pinnedPageId: user.pinnedPageId, pinnedPage: user.pinnedPage, publicReactions: user.publicReactions, - ffVisibility: user.ffVisibility, + followingVisibility: user.followingVisibility, + followersVisibility: user.followersVisibility, twoFactorEnabled: user.twoFactorEnabled, usePasswordLessLogin: user.usePasswordLessLogin, securityKeys: user.securityKeys, @@ -386,7 +387,8 @@ describe('ユーザー', () => { assert.strictEqual(response.pinnedPageId, null); assert.strictEqual(response.pinnedPage, null); assert.strictEqual(response.publicReactions, true); - assert.strictEqual(response.ffVisibility, 'public'); + assert.strictEqual(response.followingVisibility, 'public'); + assert.strictEqual(response.followersVisibility, 'public'); assert.strictEqual(response.twoFactorEnabled, false); assert.strictEqual(response.usePasswordLessLogin, false); assert.strictEqual(response.securityKeys, false); @@ -495,9 +497,12 @@ describe('ユーザー', () => { { parameters: (): object => ({ alwaysMarkNsfw: false }) }, { parameters: (): object => ({ autoSensitive: true }) }, { parameters: (): object => ({ autoSensitive: false }) }, - { parameters: (): object => ({ ffVisibility: 'private' }) }, - { parameters: (): object => ({ ffVisibility: 'followers' }) }, - { parameters: (): object => ({ ffVisibility: 'public' }) }, + { parameters: (): object => ({ followingVisibility: 'private' }) }, + { parameters: (): object => ({ followingVisibility: 'followers' }) }, + { parameters: (): object => ({ followingVisibility: 'public' }) }, + { parameters: (): object => ({ followersVisibility: 'private' }) }, + { parameters: (): object => ({ followersVisibility: 'followers' }) }, + { parameters: (): object => ({ followersVisibility: 'public' }) }, { parameters: (): object => ({ mutedWords: Array(19).fill(['xxxxx']) }) }, { parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) }, { parameters: (): object => ({ mutedWords: [] }) }, diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index c2e6ee52f3..2960489c77 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -82,7 +82,8 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi birthday: '2014-06-20', createdAt: '2016-12-28T22:49:51.000Z', description: 'I am a cool user!', - ffVisibility: 'public', + followingVisibility: 'public', + followersVisibility: 'public', roles: [], fields: [ { diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue index eaebbf03e7..762b9b4316 100644 --- a/packages/frontend/src/components/MkUserInfo.vue +++ b/packages/frontend/src/components/MkUserInfo.vue @@ -22,10 +22,10 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts.notes }}

{{ number(user.notesCount) }}
-
+

{{ i18n.ts.following }}

{{ number(user.followingCount) }}
-
+

{{ i18n.ts.followers }}

{{ number(user.followersCount) }}
@@ -40,7 +40,7 @@ import number from '@/filters/number.js'; import { userPage } from '@/filters/user.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; -import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; +import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; defineProps<{ user: Misskey.entities.UserDetailed; diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index b703369433..df8252fb14 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -35,11 +35,11 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.notes }}
{{ number(user.notesCount) }}
-
+
{{ i18n.ts.following }}
{{ number(user.followingCount) }}
-
+
{{ i18n.ts.followers }}
{{ number(user.followersCount) }}
@@ -65,7 +65,7 @@ import number from '@/filters/number.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { $i } from '@/account.js'; -import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; +import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; const props = defineProps<{ showing: boolean; diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index 971881ea24..67a2f2cb40 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -13,12 +13,18 @@ SPDX-License-Identifier: AGPL-3.0-only - - + + + + + + + + + - @@ -84,7 +90,8 @@ const preventAiLearning = ref($i.preventAiLearning); const isExplorable = ref($i.isExplorable); const hideOnlineStatus = ref($i.hideOnlineStatus); const publicReactions = ref($i.publicReactions); -const ffVisibility = ref($i.ffVisibility); +const followingVisibility = ref($i?.followingVisibility); +const followersVisibility = ref($i?.followersVisibility); const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility')); const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly')); @@ -100,7 +107,8 @@ function save() { isExplorable: !!isExplorable.value, hideOnlineStatus: !!hideOnlineStatus.value, publicReactions: !!publicReactions.value, - ffVisibility: ffVisibility.value, + followingVisibility: followingVisibility.value, + followersVisibility: followersVisibility.value, }); } diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index a87e03e761..a9497f4fe0 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -110,11 +110,11 @@ SPDX-License-Identifier: AGPL-3.0-only {{ number(user.notesCount) }} {{ i18n.ts.notes }} - + {{ number(user.followingCount) }} {{ i18n.ts.following }} - + {{ number(user.followersCount) }} {{ i18n.ts.followers }} @@ -173,7 +173,7 @@ import { dateString } from '@/filters/date.js'; import { confetti } from '@/scripts/confetti.js'; import MkNotes from '@/components/MkNotes.vue'; import { api } from '@/os.js'; -import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; +import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; function calcAge(birthdate: string): number { const date = new Date(birthdate); diff --git a/packages/frontend/src/scripts/isFfVisibleForMe.ts b/packages/frontend/src/scripts/isFfVisibleForMe.ts index 0567f3b34a..dc0e90d20a 100644 --- a/packages/frontend/src/scripts/isFfVisibleForMe.ts +++ b/packages/frontend/src/scripts/isFfVisibleForMe.ts @@ -6,11 +6,19 @@ import * as Misskey from 'misskey-js'; import { $i } from '@/account.js'; -export function isFfVisibleForMe(user: Misskey.entities.UserDetailed): boolean { +export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean { if ($i && $i.id === user.id) return true; - if (user.ffVisibility === 'private') return false; - if (user.ffVisibility === 'followers' && !user.isFollowing) return false; + if (user.followingVisibility === 'private') return false; + if (user.followingVisibility === 'followers' && !user.isFollowing) return false; + + return true; +} +export function isFollowersVisibleForMe(user: Misskey.entities.UserDetailed): boolean { + if ($i && $i.id === user.id) return true; + + if (user.followersVisibility === 'private') return false; + if (user.followersVisibility === 'followers' && !user.isFollowing) return false; return true; } diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index a8f0b96d5d..83d313a5fe 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -4,7 +4,9 @@ export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; -export const ffVisibility = ['public', 'followers', 'private'] as const; +export const followingVisibilities = ['public', 'followers', 'private'] as const; + +export const followersVisibilities = ['public', 'followers', 'private'] as const; export const permissions = [ 'read:account', diff --git a/packages/misskey-js/src/index.ts b/packages/misskey-js/src/index.ts index e78501fdfd..54cae8ec03 100644 --- a/packages/misskey-js/src/index.ts +++ b/packages/misskey-js/src/index.ts @@ -16,7 +16,8 @@ export const permissions = consts.permissions; export const notificationTypes = consts.notificationTypes; export const noteVisibilities = consts.noteVisibilities; export const mutedNoteReasons = consts.mutedNoteReasons; -export const ffVisibility = consts.ffVisibility; +export const followingVisibilities = consts.followingVisibilities; +export const followersVisibilities = consts.followersVisibilities; export const moderationLogTypes = consts.moderationLogTypes; // api extractor not supported yet -- cgit v1.2.3-freya