parent
49dc9e63e7
commit
4299588806
@ -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)
|
||||
|
@ -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"
|
||||
|
38
locales/index.d.ts
vendored
38
locales/index.d.ts
vendored
@ -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;
|
||||
|
@ -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: "全てのノート"
|
||||
|
@ -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"`);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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<typeof meta, typeof paramDef> { // 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);
|
||||
});
|
||||
}
|
||||
|
@ -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<typeof meta, typeof paramDef> { // 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);
|
||||
});
|
||||
}
|
||||
|
@ -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<typeof meta, typeof paramDef> { // 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);
|
||||
});
|
||||
}
|
||||
|
@ -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<typeof meta, typeof paramDef> { // 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);
|
||||
});
|
||||
}
|
||||
|
@ -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<typeof meta, typeof paramDef> { // 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);
|
||||
});
|
||||
}
|
||||
|
@ -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<typeof meta, typeof paramDef> { // 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;
|
||||
|
@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -196,9 +196,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<div v-else-if="tab === 'reactions'" :class="$style.tab_reactions">
|
||||
<div :class="$style.reactionTabs">
|
||||
<button v-for="reaction in Object.keys(appearNote.reactions)" :key="reaction" :class="[$style.reactionTab, { [$style.reactionTabActive]: reactionTabType === reaction }]" class="_button" @click="reactionTabType = reaction">
|
||||
<button v-for="reaction in Object.keys(appearNote.reactions)" :key="reaction" :class="[$style.reactionTab, { [$style.reactionTabActive]: reactionTabType === reaction }]" class="_button" @click="reactionTabType = defaultStore.state.hideReactionUsers ? null : reaction">
|
||||
<MkReactionIcon :reaction="reaction"/>
|
||||
<span style="margin-left: 4px;">{{ appearNote.reactions[reaction] }}</span>
|
||||
<span v-if="!hideReactionCount" style="margin-left: 4px;">{{ appearNote.reactions[reaction] }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<MkButton v-if="reactionTabType" :class="$style.reactionMuteButton" @click="reactionMuteToggle(reactionTabTypeTrimLocal)">
|
||||
@ -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<OpenOnRemoteOptions>(() => ({
|
||||
type: 'lookup',
|
||||
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkReactionIcon :reaction="reaction" :class="$style.reactionIcon" :noStyle="true"/>
|
||||
<div :class="$style.reactionName">{{ getReactionName(reaction) }}</div>
|
||||
</div>
|
||||
<div :class="$style.users">
|
||||
<div v-if="users.length" :class="$style.users">
|
||||
<div v-for="u in users" :key="u.id" :class="$style.user">
|
||||
<MkAvatar :class="$style.avatar" :user="u"/>
|
||||
<MkUserName :user="u" :nowrap="true"/>
|
||||
@ -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 {
|
||||
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
@contextmenu.prevent.stop="menu"
|
||||
>
|
||||
<MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]" @click="toggleReaction()" @click.stop/>
|
||||
<span :class="$style.count">{{ count }}</span>
|
||||
<span v-if="!hideReactionCount" :class="$style.count">{{ count }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@ -56,11 +56,29 @@ const buttonEl = shallowRef<HTMLElement>();
|
||||
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;
|
||||
|
@ -204,7 +204,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<div v-else-if="tab === 'reactions'" :class="$style.tab_reactions">
|
||||
<div :class="$style.reactionTabs">
|
||||
<button v-for="reaction in Object.keys(appearNote.reactions)" :key="reaction" :class="[$style.reactionTab, { [$style.reactionTabActive]: reactionTabType === reaction }]" class="_button" @click="reactionTabType = reaction">
|
||||
<button v-for="reaction in Object.keys(appearNote.reactions)" :key="reaction" :class="[$style.reactionTab, { [$style.reactionTabActive]: reactionTabType === reaction }]" class="_button" @click="reactionTabType = defaultStore.state.hideReactionUsers ? null : reaction">
|
||||
<MkReactionIcon :reaction="reaction"/>
|
||||
<span style="margin-left: 4px;">{{ appearNote.reactions[reaction] }}</span>
|
||||
</button>
|
||||
|
@ -103,6 +103,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<option value="misskey"><i class="sk-icons sk-misskey sk-icons-lg" style="top: 2px;position: relative;"></i> Misskey</option>
|
||||
</MkRadios>
|
||||
<MkSwitch v-model="limitWidthOfReaction">{{ i18n.ts.limitWidthOfReaction }}</MkSwitch>
|
||||
<MkSwitch v-model="hideReactionUsers">
|
||||
<template #caption>{{ i18n.ts.hideReactionUsersDescription }}</template>
|
||||
{{ i18n.ts.hideReactionUsers }}
|
||||
<span class="_beta">{{ i18n.ts.originalFeature }}</span>
|
||||
</MkSwitch>
|
||||
<MkSelect v-model="hideReactionCount">
|
||||
<template #label>{{ i18n.ts.hideReactionCount }}<span class="_beta">{{ i18n.ts.originalFeature }}</span></template>
|
||||
<option value="none">{{ i18n.ts._hideReactionCount.none }}</option>
|
||||
<option value="self">{{ i18n.ts._hideReactionCount.self }}</option>
|
||||
<option value="others">{{ i18n.ts._hideReactionCount.others }}</option>
|
||||
<option value="all">{{ i18n.ts._hideReactionCount.all }}</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
|
||||
<MkSelect v-model="instanceTicker">
|
||||
@ -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,
|
||||
|
@ -17,6 +17,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #caption>{{ i18n.ts.makeReactionsPublicDescription }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="hideActivity" @update:modelValue="save()">
|
||||
{{ i18n.ts.hideActivity }}<span class="_beta">{{ i18n.ts.originalFeature }}</span>
|
||||
<template #caption>{{ i18n.ts.hideActivityDescription }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSelect v-model="followingVisibility" @update:modelValue="save()">
|
||||
<template #label>{{ i18n.ts.followingVisibility }}</template>
|
||||
<option value="public">{{ i18n.ts._ffVisibility.public }}</option>
|
||||
@ -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,
|
||||
});
|
||||
|
@ -156,7 +156,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;">
|
||||
<XFiles :key="user.id" :user="user"/>
|
||||
<XActivity :key="user.id" :user="user"/>
|
||||
<XActivity v-if="!user.hideActivity" :key="user.id" :user="user"/>
|
||||
<XListenBrainz v-if="user.listenbrainz && listenbrainzdata" :key="user.id" :user="user"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user