From 4299588806488fcd4c2f7b43e1f0da25aeb504a3 Mon Sep 17 00:00:00 2001 From: lqvp <183242690+lqvp@users.noreply.github.com> Date: Thu, 3 Oct 2024 06:47:44 +0900 Subject: [PATCH] Merge pull request #39 from lqvp/master MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit プライバシーの強化 --- DIFFERENCE.md | 3 ++ locales/en-US.yml | 11 ++++++ locales/index.d.ts | 38 +++++++++++++++++++ locales/ja-JP.yml | 10 +++++ .../1710146785085-feat-hide-activity.js | 11 ++++++ .../src/core/entities/UserEntityService.ts | 2 + packages/backend/src/models/UserProfile.ts | 5 +++ .../backend/src/models/json-schema/user.ts | 4 ++ .../server/api/endpoints/charts/user/drive.ts | 26 ++++++++++++- .../api/endpoints/charts/user/following.ts | 26 ++++++++++++- .../server/api/endpoints/charts/user/notes.ts | 26 ++++++++++++- .../server/api/endpoints/charts/user/pv.ts | 26 ++++++++++++- .../api/endpoints/charts/user/reactions.ts | 26 ++++++++++++- .../src/server/api/endpoints/i/update.ts | 4 +- .../server/api/endpoints/users/followers.ts | 35 ++++++++--------- .../server/api/endpoints/users/following.ts | 35 ++++++++--------- .../src/components/MkNoteDetailed.vue | 13 ++++++- .../components/MkReactionsViewer.details.vue | 6 +-- .../components/MkReactionsViewer.reaction.vue | 24 ++++++++++-- .../src/components/SkNoteDetailed.vue | 2 +- .../frontend/src/pages/settings/general.vue | 15 ++++++++ .../frontend/src/pages/settings/privacy.vue | 7 ++++ packages/frontend/src/pages/user/home.vue | 2 +- packages/frontend/src/pages/user/index.vue | 4 +- packages/misskey-js/src/autogen/types.ts | 2 + 25 files changed, 305 insertions(+), 58 deletions(-) create mode 100644 packages/backend/migration/1710146785085-feat-hide-activity.js diff --git a/DIFFERENCE.md b/DIFFERENCE.md index 6365923051..00aab04f23 100755 --- a/DIFFERENCE.md +++ b/DIFFERENCE.md @@ -1,4 +1,5 @@ # DIFFRENCE +<<<<<<< HEAD ## 2024.9.0-yami-1.3.1 ## Client - フォロー/フォロワー/アナウンス/みつける/Play/ギャラリー/チャンネル/TL/ユーザー/ノートのページをログイン必須に @@ -12,6 +13,8 @@ ## Feat - ノート数を隠せるように(連合しません) +======= +>>>>>>> 00cf91cf30 (Merge pull request #39 from lqvp/master) ## 2024.9.0-yami-1.2.8 ## Feat - Cherry-Pick アクティビティの非公開機能(hideki0403/kakurega.app) diff --git a/locales/en-US.yml b/locales/en-US.yml index e71b841224..46e72a8ba8 100755 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1344,6 +1344,12 @@ _delivery: autoSuspendedForNotResponding: "Server is suspended due to no responding" scheduledNoteDelete: "Time Bomb" noteDeletationAt: "This note will be deleted at {time}" +hideActivity: "Hide Activity" +hideActivityDescription: "This option prevents others from viewing your activity on your profile (Summary/Activity tab). Even with this option enabled, you can still view your activity from the Activity tab on your profile." +hideReactionUsers: "Hide Users Who Reacted" +hideReactionUsersDescription: "This option hides the list of users who reacted when hovering over the reaction and the list of users who reacted in the reactions tab on the note detail page." +hideReactionCount: "Hide Reaction Count" + _bubbleGame: howToPlay: "How to play" @@ -2831,3 +2837,8 @@ _contextMenu: _reactionChecksMuting: title: "Check mutings when get reactions" caption: "Check mutings when get reactions, but cache does not work and may increase traffic" +_hideReactionCount: + none: "Do not hide" + self: "Only my notes" + others: "Only notes of others" + all: "All notes" diff --git a/locales/index.d.ts b/locales/index.d.ts index 714e73cecb..7d67454f51 100755 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5401,6 +5401,26 @@ export interface Locale extends ILocale { "autoSuspendedForNotResponding": string; }; }; + /** + * アクティビティを非公開にする + */ + "hideActivity": string; + /** + * 自分のプロフィールのアクティビティ (概要/アクティビティタブ) を他人が見れないようにします。このオプションを有効にしても、自分であればプロフィールのアクティビティタブから引き続き閲覧できます。 + */ + "hideActivityDescription": string; + /** + * 誰がリアクションをしたのかを非表示にする + */ + "hideReactionUsers": string; + /** + * リアクションをホバーした際のユーザー一覧と、ノート詳細ページのリアクションタブにあるリアクションをしたユーザー一覧を非表示にします + */ + "hideReactionUsersDescription": string; + /** + * リアクション数の非表示 + */ + "hideReactionCount": string; "_bubbleGame": { /** * 遊び方 @@ -10937,6 +10957,24 @@ export interface Locale extends ILocale { */ "caption": string; }; + "_hideReactionCount": { + /** + * 非表示にしない + */ + "none": string; + /** + * 自分のノートのみ + */ + "self": string; + /** + * 自分以外のノートのみ + */ + "others": string; + /** + * 全てのノート + */ + "all": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 21ef77277a..538f93ad75 100755 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1348,6 +1348,11 @@ defaultScheduledNoteDeleteTime: "ノートの自己消滅の初期値" scheduledNoteDeleteEnabled: "ノートの自己消滅が有効になっています" cannotScheduleLaterThanOneYear: "1年以上先の日時を指定することはできません" defaultScheduledNoteDelete: "デフォルトでノートが自己消滅するように" +hideActivity: "アクティビティを非公開にする" +hideActivityDescription: "自分のプロフィールのアクティビティ (概要/アクティビティタブ) を他人が見れないようにします。このオプションを有効にしても、自分であればプロフィールのアクティビティタブから引き続き閲覧できます。" +hideReactionUsers: "誰がリアクションをしたのかを非表示にする" +hideReactionUsersDescription: "リアクションをホバーした際のユーザー一覧と、ノート詳細ページのリアクションタブにあるリアクションをしたユーザー一覧を非表示にします" +hideReactionCount: "リアクション数の非表示" _bubbleGame: howToPlay: "遊び方" @@ -2911,3 +2916,8 @@ _contextMenu: _reactionChecksMuting: title: "リアクションでミュートを考慮する" caption: "リアクションがミュートを考慮しますが、キャッシュが効かず通信量が増えることがあります。" +_hideReactionCount: + none: "非表示にしない" + self: "自分のノートのみ" + others: "自分以外のノートのみ" + all: "全てのノート" diff --git a/packages/backend/migration/1710146785085-feat-hide-activity.js b/packages/backend/migration/1710146785085-feat-hide-activity.js new file mode 100644 index 0000000000..fb2e060a3b --- /dev/null +++ b/packages/backend/migration/1710146785085-feat-hide-activity.js @@ -0,0 +1,11 @@ +export class FeatHideActivity1710146785085 { + name = 'FeatHideActivity1710146785085' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "hideActivity" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hideActivity"`); + } +} diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index a83c42ba58..a16b2b7d41 100755 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -587,6 +587,7 @@ export class UserEntityService implements OnModuleInit { pinnedPageId: profile!.pinnedPageId, pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null, publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964 + hideActivity: this.isLocalUser(user) ? profile!.hideActivity : false, // followersVisibility: profile!.followersVisibility, followingVisibility: profile!.followingVisibility, twoFactorEnabled: profile!.twoFactorEnabled, @@ -627,6 +628,7 @@ export class UserEntityService implements OnModuleInit { isDeleted: user.isDeleted, twoFactorBackupCodesStock: profile?.twoFactorBackupSecret?.length === 5 ? 'full' : (profile?.twoFactorBackupSecret?.length ?? 0) > 0 ? 'partial' : 'none', hideOnlineStatus: user.hideOnlineStatus, + enableGTL: profile!.enableGTL, hasUnreadSpecifiedNotes: this.noteUnreadsRepository.count({ where: { userId: user.id, isSpecified: true }, take: 1, diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index c6bf2bc017..34cc86d49f 100755 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -112,6 +112,11 @@ export class MiUserProfile { }) public followersVisibility: typeof followersVisibilities[number]; + @Column('boolean', { + default: false, + }) + public hideActivity: boolean; + @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 33a3efd453..1806c3dace 100755 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -369,6 +369,10 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'boolean', nullable: false, optional: false, }, + hideActivity: { + type: 'boolean', + nullable: false, optional: false, + }, followingVisibility: { type: 'string', nullable: false, optional: false, diff --git a/packages/backend/src/server/api/endpoints/charts/user/drive.ts b/packages/backend/src/server/api/endpoints/charts/user/drive.ts index dcb72084b7..f5d469379e 100755 --- a/packages/backend/src/server/api/endpoints/charts/user/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/drive.ts @@ -3,11 +3,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import type { UserProfilesRepository } from '@/models/_.js'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import { schema } from '@/core/chart/charts/entities/per-user-drive.js'; +import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['charts', 'drive', 'users'], @@ -16,6 +20,14 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + errors: { + activityNotPublic: { + message: 'Activity of the user is not public.', + code: 'ACTIVITY_NOT_PUBLIC', + id: '28e59b25-7eaf-4ff4-bac5-251fd7d8449b', + }, + }, } as const; export const paramDef = { @@ -32,9 +44,21 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private roleService: RoleService, private perUserDriveChart: PerUserDriveChart, ) { super(meta, paramDef, async (ps, me) => { + const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see activity of all users + if (!iAmModerator) { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId }); + if ((me == null || me.id !== ps.userId) && profile.hideActivity) { + throw new ApiError(meta.errors.activityNotPublic); + } + } + return await this.perUserDriveChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId); }); } diff --git a/packages/backend/src/server/api/endpoints/charts/user/following.ts b/packages/backend/src/server/api/endpoints/charts/user/following.ts index 0a019ce4fb..e77cd3821e 100755 --- a/packages/backend/src/server/api/endpoints/charts/user/following.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/following.ts @@ -3,11 +3,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import type { UserProfilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { getJsonSchema } from '@/core/chart/core.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import { schema } from '@/core/chart/charts/entities/per-user-following.js'; +import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['charts', 'users', 'following'], @@ -16,6 +20,14 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + errors: { + activityNotPublic: { + message: 'Activity of the user is not public.', + code: 'ACTIVITY_NOT_PUBLIC', + id: '28e59b25-7eaf-4ff4-bac5-251fd7d8449b', + }, + }, } as const; export const paramDef = { @@ -32,9 +44,21 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private roleService: RoleService, private perUserFollowingChart: PerUserFollowingChart, ) { super(meta, paramDef, async (ps, me) => { + const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see activity of all users + if (!iAmModerator) { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId }); + if ((me == null || me.id !== ps.userId) && profile.hideActivity) { + throw new ApiError(meta.errors.activityNotPublic); + } + } + return await this.perUserFollowingChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId); }); } diff --git a/packages/backend/src/server/api/endpoints/charts/user/notes.ts b/packages/backend/src/server/api/endpoints/charts/user/notes.ts index 06b15bca18..9fcaa461b7 100755 --- a/packages/backend/src/server/api/endpoints/charts/user/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/notes.ts @@ -3,11 +3,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import type { UserProfilesRepository } from '@/models/_.js'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; import { schema } from '@/core/chart/charts/entities/per-user-notes.js'; +import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['charts', 'users', 'notes'], @@ -16,6 +20,14 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + errors: { + activityNotPublic: { + message: 'Activity of the user is not public.', + code: 'ACTIVITY_NOT_PUBLIC', + id: '28e59b25-7eaf-4ff4-bac5-251fd7d8449b', + }, + }, } as const; export const paramDef = { @@ -32,9 +44,21 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private roleService: RoleService, private perUserNotesChart: PerUserNotesChart, ) { super(meta, paramDef, async (ps, me) => { + const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see activity of all users + if (!iAmModerator) { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId }); + if ((me == null || me.id !== ps.userId) && profile.hideActivity) { + throw new ApiError(meta.errors.activityNotPublic); + } + } + return await this.perUserNotesChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId); }); } diff --git a/packages/backend/src/server/api/endpoints/charts/user/pv.ts b/packages/backend/src/server/api/endpoints/charts/user/pv.ts index d359b491e2..af91f56d52 100755 --- a/packages/backend/src/server/api/endpoints/charts/user/pv.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/pv.ts @@ -3,11 +3,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import type { UserProfilesRepository } from '@/models/_.js'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import PerUserPvChart from '@/core/chart/charts/per-user-pv.js'; import { schema } from '@/core/chart/charts/entities/per-user-pv.js'; +import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['charts', 'users'], @@ -16,6 +20,14 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + errors: { + activityNotPublic: { + message: 'Activity of the user is not public.', + code: 'ACTIVITY_NOT_PUBLIC', + id: '28e59b25-7eaf-4ff4-bac5-251fd7d8449b', + }, + }, } as const; export const paramDef = { @@ -32,9 +44,21 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private roleService: RoleService, private perUserPvChart: PerUserPvChart, ) { super(meta, paramDef, async (ps, me) => { + const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see activity of all users + if (!iAmModerator) { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId }); + if ((me == null || me.id !== ps.userId) && profile.hideActivity) { + throw new ApiError(meta.errors.activityNotPublic); + } + } + return await this.perUserPvChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId); }); } diff --git a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts index 4355aa5348..ba2b7b3cd1 100755 --- a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts @@ -3,11 +3,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import type { UserProfilesRepository } from '@/models/_.js'; import { getJsonSchema } from '@/core/chart/core.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js'; import { schema } from '@/core/chart/charts/entities/per-user-reactions.js'; +import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['charts', 'users', 'reactions'], @@ -16,6 +20,14 @@ export const meta = { allowGet: true, cacheSec: 60 * 60, + + errors: { + activityNotPublic: { + message: 'Activity of the user is not public.', + code: 'ACTIVITY_NOT_PUBLIC', + id: '28e59b25-7eaf-4ff4-bac5-251fd7d8449b', + }, + }, } as const; export const paramDef = { @@ -32,9 +44,21 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private roleService: RoleService, private perUserReactionsChart: PerUserReactionsChart, ) { super(meta, paramDef, async (ps, me) => { + const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see activity of all users + if (!iAmModerator) { + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId }); + if ((me == null || me.id !== ps.userId) && profile.hideActivity) { + throw new ApiError(meta.errors.activityNotPublic); + } + } + return await this.perUserReactionsChart.getChart(ps.span, ps.limit, ps.offset ? new Date(ps.offset) : null, ps.userId); }); } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 44affc2e00..2e940894a5 100755 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -181,6 +181,7 @@ export const paramDef = { isExplorable: { type: 'boolean' }, hideOnlineStatus: { type: 'boolean' }, publicReactions: { type: 'boolean' }, + hideActivity: { type: 'boolean' }, carefulBot: { type: 'boolean' }, autoAcceptFollowed: { type: 'boolean' }, autoRejectFollowRequest: { type: 'boolean' }, @@ -335,6 +336,7 @@ export default class extends Endpoint { // eslint- if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus; if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions; if (typeof ps.noindex === 'boolean') updates.noindex = ps.noindex; + if (typeof ps.hideActivity === 'boolean') profileUpdates.hideActivity = ps.hideActivity; if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot; if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; @@ -401,7 +403,7 @@ export default class extends Endpoint { // eslint- updates.backgroundUrl = null; updates.backgroundBlurhash = null; } - + if (ps.avatarDecorations) { policies ??= await this.roleService.getUserPolicies(user.id); const decorations = await this.avatarDecorationService.getAll(true); diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index a8b4319a61..7634abf236 100755 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -11,7 +11,6 @@ import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; -import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -82,7 +81,6 @@ export default class extends Endpoint { // eslint- private utilityService: UtilityService, private followingEntityService: FollowingEntityService, private queryService: QueryService, - private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy(ps.userId != null @@ -95,24 +93,21 @@ export default class extends Endpoint { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.followersVisibility !== 'public' && !await this.roleService.isModerator(me)) { - if (profile.followersVisibility === 'private') { - if (me == null || (me.id !== user.id)) { - throw new ApiError(meta.errors.forbidden); - } - } else if (profile.followersVisibility === 'followers') { - if (me == null) { - throw new ApiError(meta.errors.forbidden); - } else if (me.id !== user.id) { - const isFollowing = await this.followingsRepository.exists({ - where: { - followeeId: user.id, - followerId: me.id, - }, - }); - if (!isFollowing) { - throw new ApiError(meta.errors.forbidden); - } + if (profile.followersVisibility === 'private') { + if (me == null || (me.id !== user.id)) { + throw new ApiError(meta.errors.forbidden); + } + } else if (profile.followersVisibility === 'followers') { + if (me == null) { + throw new ApiError(meta.errors.forbidden); + } else if (me.id !== user.id) { + const isFollowing = await this.followingsRepository.exists({ + where: { + followeeId: user.id, + followerId: me.id, + }, + }); + if (!isFollowing) { } } } diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index feda5bb353..2ab31a435c 100755 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -12,7 +12,6 @@ import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; -import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -91,7 +90,6 @@ export default class extends Endpoint { // eslint- private utilityService: UtilityService, private followingEntityService: FollowingEntityService, private queryService: QueryService, - private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy(ps.userId != null @@ -104,24 +102,21 @@ export default class extends Endpoint { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.followingVisibility !== 'public' && !await this.roleService.isModerator(me)) { - if (profile.followingVisibility === 'private') { - if (me == null || (me.id !== user.id)) { - throw new ApiError(meta.errors.forbidden); - } - } else if (profile.followingVisibility === 'followers') { - if (me == null) { - throw new ApiError(meta.errors.forbidden); - } else if (me.id !== user.id) { - const isFollowing = await this.followingsRepository.exists({ - where: { - followeeId: user.id, - followerId: me.id, - }, - }); - if (!isFollowing) { - throw new ApiError(meta.errors.forbidden); - } + if (profile.followingVisibility === 'private') { + if (me == null || (me.id !== user.id)) { + throw new ApiError(meta.errors.forbidden); + } + } else if (profile.followingVisibility === 'followers') { + if (me == null) { + throw new ApiError(meta.errors.forbidden); + } else if (me.id !== user.id) { + const isFollowing = await this.followingsRepository.exists({ + where: { + followeeId: user.id, + followerId: me.id, + }, + }); + if (!isFollowing) { } } } diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 62030f748e..1e33dc75b9 100755 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -196,9 +196,9 @@ SPDX-License-Identifier: AGPL-3.0-only
-
@@ -352,6 +352,15 @@ if ($i) { } let renoting = false; +const hideReactionCount = computed(() => { + switch (defaultStore.state.hideReactionCount) { + case 'none': return false; + case 'all': return true; + case 'self': return props.note.userId === $i?.id; + case 'others': return props.note.userId !== $i?.id; + default: return false; + } +}); const pleaseLoginContext = computed(() => ({ type: 'lookup', diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue index aa5cbb73a0..9b33aad5e0 100755 --- a/packages/frontend/src/components/MkReactionsViewer.details.vue +++ b/packages/frontend/src/components/MkReactionsViewer.details.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ getReactionName(reaction) }}
-
+
@@ -57,9 +57,7 @@ function getReactionName(reaction: string): string { .reaction { max-width: 100px; - padding-right: 10px; text-align: center; - border-right: solid 0.5px var(--divider); } .reactionIcon { @@ -80,6 +78,8 @@ function getReactionName(reaction: string): string { margin: -4px 14px 0 10px; font-size: 0.95em; text-align: left; + padding-left: 10px; + border-left: solid 0.5px var(--divider); } .user { diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 3688801791..f6cdb6597c 100755 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only @contextmenu.prevent.stop="menu" > - {{ count }} + {{ count }} @@ -56,11 +56,29 @@ const buttonEl = shallowRef(); const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, '')); const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction)); +function getReactionName(reaction: string, formated = false) { + const r = reaction.replaceAll(':', '').replace(/@.*/, ''); + return formated ? `:${r}:` : r; +} + +const isLocal = computed(() => !props.reaction.match(/@\w/)); +const isAvailable = computed(() => isLocal.value ? true : customEmojisMap.has(getReactionName(props.reaction))); + const canToggle = computed(() => { return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value); }); const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':')); +const hideReactionCount = computed(() => { + switch (defaultStore.state.hideReactionCount) { + case 'none': return false; + case 'all': return true; + case 'self': return props.note.userId === $i?.id; + case 'others': return props.note.userId !== $i?.id; + default: return false; + } +}); + async function toggleReaction() { if (!canToggle.value) return; @@ -150,12 +168,12 @@ if (!mock) { useTooltip(buttonEl, async (showing) => { const useGet = !reactionChecksMuting.value; const apiCall = useGet ? misskeyApiGet : misskeyApi; - const reactions = await apiCall('notes/reactions', { + const reactions = !defaultStore.state.hideReactionUsers ? await apiCall('notes/reactions', { noteId: props.note.id, type: props.reaction, limit: 10, _cacheKey_: props.count, - }); + }) : []; const users = reactions.map(x => x.user); const count = users.length; diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue index 494a0635d6..748e1ffb99 100755 --- a/packages/frontend/src/components/SkNoteDetailed.vue +++ b/packages/frontend/src/components/SkNoteDetailed.vue @@ -204,7 +204,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index e2a4137105..829cd0f89b 100755 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -103,6 +103,18 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.limitWidthOfReaction }} + + + {{ i18n.ts.hideReactionUsers }} + {{ i18n.ts.originalFeature }} + + + + + + + +
@@ -382,6 +394,8 @@ const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNot const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter')); const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize')); const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthOfReaction')); +const hideReactionUsers = computed(defaultStore.makeGetterSetter('hideReactionUsers')); +const hideReactionCount = computed(defaultStore.makeGetterSetter('hideReactionCount')); const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes')); const collapseNotesRepliedTo = computed(defaultStore.makeGetterSetter('collapseNotesRepliedTo')); const clickToOpen = computed(defaultStore.makeGetterSetter('clickToOpen')); @@ -494,6 +508,7 @@ watch([ showGapBetweenNotesInTimeline, instanceTicker, instanceIcon, + hideReactionCount, overridedDeviceKind, mediaListWithOneImageAppearance, reactionsDisplaySize, diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index 1a0a24b8ab..eca135cf30 100755 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -17,6 +17,11 @@ SPDX-License-Identifier: AGPL-3.0-only + + {{ i18n.ts.hideActivity }}{{ i18n.ts.originalFeature }} + + + @@ -97,6 +102,7 @@ const noindex = ref($i.noindex); const isExplorable = ref($i.isExplorable); const hideOnlineStatus = ref($i.hideOnlineStatus); const publicReactions = ref($i.publicReactions); +const hideActivity = ref($i.hideActivity); const followingVisibility = ref($i.followingVisibility); const followersVisibility = ref($i.followersVisibility); @@ -115,6 +121,7 @@ function save() { isExplorable: !!isExplorable.value, hideOnlineStatus: !!hideOnlineStatus.value, publicReactions: !!publicReactions.value, + hideActivity: !!hideActivity.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 b997fe1c3f..e5c428897b 100755 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -156,7 +156,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue index 87c8bb2866..1f9d0d7208 100755 --- a/packages/frontend/src/pages/user/index.vue +++ b/packages/frontend/src/pages/user/index.vue @@ -88,11 +88,11 @@ const headerTabs = computed(() => user.value ? [{ key: 'notes', title: i18n.ts.notes, icon: 'ti ti-pencil', -}, { +}, ...($i && ($i.id === user.value.id || $i.isAdmin || $i.isModerator)) || !user.value.hideActivity ? [{ key: 'activity', title: i18n.ts.activity, icon: 'ti ti-chart-line', -}, ...(user.value.host == null ? [{ +}] : [], ...(user.value.host == null ? [{ key: 'achievements', title: i18n.ts.achievements, icon: 'ti ti-medal', diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index b443da4882..67c04cc2c2 100755 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3888,6 +3888,7 @@ export type components = { pinnedPageId: string | null; pinnedPage: components['schemas']['Page'] | null; publicReactions: boolean; + hideActivity: boolean; /** @enum {string} */ followingVisibility: 'public' | 'followers' | 'private'; /** @enum {string} */ @@ -20239,6 +20240,7 @@ export type operations = { isExplorable?: boolean; hideOnlineStatus?: boolean; publicReactions?: boolean; + hideActivity?: boolean; carefulBot?: boolean; autoAcceptFollowed?: boolean; noCrawle?: boolean;