feat: フォローされた際のメッセージを設定できるようにする (#14430)
* feat: フォローされた際のメッセージを設定できるようにする Resolve #14425 * Update CHANGELOG.md * 既にフォローしているユーザーのメッセージも見れるように * Update packages/frontend/src/components/MkNotification.vue Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> * fix indent * Update users.ts * wip * Update users.ts --------- Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
This commit is contained in:
parent
e4d4cc5277
commit
28e9d4e483
@ -4,6 +4,7 @@
|
|||||||
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
|
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
|
||||||
- 埋め込みコードやウェブサイトへの実装方法の詳細は https://misskey-hub.net/docs/for-users/features/embed/ をご覧ください
|
- 埋め込みコードやウェブサイトへの実装方法の詳細は https://misskey-hub.net/docs/for-users/features/embed/ をご覧ください
|
||||||
- Feat: パスキーでログインボタンを実装 (#14574)
|
- Feat: パスキーでログインボタンを実装 (#14574)
|
||||||
|
- Feat: フォローされた際のメッセージを設定できるように
|
||||||
- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445)
|
- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445)
|
||||||
- Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように
|
- Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように
|
||||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/680)
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/680)
|
||||||
|
12
locales/index.d.ts
vendored
12
locales/index.d.ts
vendored
@ -8725,6 +8725,18 @@ export interface Locale extends ILocale {
|
|||||||
* 最大{max}つまでデコレーションを付けられます。
|
* 最大{max}つまでデコレーションを付けられます。
|
||||||
*/
|
*/
|
||||||
"avatarDecorationMax": ParameterizedString<"max">;
|
"avatarDecorationMax": ParameterizedString<"max">;
|
||||||
|
/**
|
||||||
|
* フォローされた時のメッセージ
|
||||||
|
*/
|
||||||
|
"followedMessage": string;
|
||||||
|
/**
|
||||||
|
* フォローされた時に相手に表示するメッセージを設定できます。
|
||||||
|
*/
|
||||||
|
"followedMessageDescription": string;
|
||||||
|
/**
|
||||||
|
* フォローを承認制にしている場合、フォローリクエストを許可した時に表示されます。
|
||||||
|
*/
|
||||||
|
"followedMessageDescriptionForLockedAccount": string;
|
||||||
};
|
};
|
||||||
"_exportOrImport": {
|
"_exportOrImport": {
|
||||||
/**
|
/**
|
||||||
|
@ -2297,6 +2297,9 @@ _profile:
|
|||||||
changeBanner: "バナー画像を変更"
|
changeBanner: "バナー画像を変更"
|
||||||
verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。"
|
verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。"
|
||||||
avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。"
|
avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。"
|
||||||
|
followedMessage: "フォローされた時のメッセージ"
|
||||||
|
followedMessageDescription: "フォローされた時に相手に表示するメッセージを設定できます。"
|
||||||
|
followedMessageDescriptionForLockedAccount: "フォローを承認制にしている場合、フォローリクエストを許可した時に表示されます。"
|
||||||
|
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "全てのノート"
|
allNotes: "全てのノート"
|
||||||
|
16
packages/backend/migration/1723944246767-followedMessage.js
Normal file
16
packages/backend/migration/1723944246767-followedMessage.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class FollowedMessage1723944246767 {
|
||||||
|
name = 'FollowedMessage1723944246767';
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query('ALTER TABLE "user_profile" ADD "followedMessage" character varying(256)');
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query('ALTER TABLE "user_profile" DROP COLUMN "followedMessage"');
|
||||||
|
}
|
||||||
|
}
|
@ -275,16 +275,19 @@ export class UserFollowingService implements OnModuleInit {
|
|||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 通知を作成
|
|
||||||
if (follower.host === null) {
|
|
||||||
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
|
||||||
}, followee.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alreadyFollowed) return;
|
if (alreadyFollowed) return;
|
||||||
|
|
||||||
|
// 通知を作成
|
||||||
|
if (follower.host === null) {
|
||||||
|
const profile = await this.cacheService.userProfileCache.fetch(followee.id);
|
||||||
|
|
||||||
|
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||||||
|
message: profile.followedMessage,
|
||||||
|
}, followee.id);
|
||||||
|
}
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
|
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
|
||||||
|
|
||||||
const [followeeUser, followerUser] = await Promise.all([
|
const [followeeUser, followerUser] = await Promise.all([
|
||||||
|
@ -494,6 +494,7 @@ export class ApRendererService {
|
|||||||
name: user.name,
|
name: user.name,
|
||||||
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
||||||
_misskey_summary: profile.description,
|
_misskey_summary: profile.description,
|
||||||
|
_misskey_followedMessage: profile.followedMessage,
|
||||||
icon: avatar ? this.renderImage(avatar) : null,
|
icon: avatar ? this.renderImage(avatar) : null,
|
||||||
image: banner ? this.renderImage(banner) : null,
|
image: banner ? this.renderImage(banner) : null,
|
||||||
tag,
|
tag,
|
||||||
|
@ -554,6 +554,7 @@ const extension_context_definition = {
|
|||||||
'_misskey_reaction': 'misskey:_misskey_reaction',
|
'_misskey_reaction': 'misskey:_misskey_reaction',
|
||||||
'_misskey_votes': 'misskey:_misskey_votes',
|
'_misskey_votes': 'misskey:_misskey_votes',
|
||||||
'_misskey_summary': 'misskey:_misskey_summary',
|
'_misskey_summary': 'misskey:_misskey_summary',
|
||||||
|
'_misskey_followedMessage': 'misskey:_misskey_followedMessage',
|
||||||
'isCat': 'misskey:isCat',
|
'isCat': 'misskey:isCat',
|
||||||
// vcard
|
// vcard
|
||||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||||
|
@ -45,7 +45,7 @@ import type { ApNoteService } from './ApNoteService.js';
|
|||||||
import type { ApMfmService } from '../ApMfmService.js';
|
import type { ApMfmService } from '../ApMfmService.js';
|
||||||
import type { ApResolverService, Resolver } from '../ApResolverService.js';
|
import type { ApResolverService, Resolver } from '../ApResolverService.js';
|
||||||
import type { ApLoggerService } from '../ApLoggerService.js';
|
import type { ApLoggerService } from '../ApLoggerService.js';
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
||||||
import type { ApImageService } from './ApImageService.js';
|
import type { ApImageService } from './ApImageService.js';
|
||||||
import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
|
import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js';
|
||||||
|
|
||||||
@ -307,8 +307,8 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
|
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
|
||||||
}
|
}
|
||||||
return 'private';
|
return 'private';
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||||
@ -370,6 +370,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
await transactionalEntityManager.save(new MiUserProfile({
|
await transactionalEntityManager.save(new MiUserProfile({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
description: _description,
|
description: _description,
|
||||||
|
followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null,
|
||||||
url,
|
url,
|
||||||
fields,
|
fields,
|
||||||
followingVisibility,
|
followingVisibility,
|
||||||
@ -494,8 +495,8 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return 'private';
|
return 'private';
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||||
@ -566,6 +567,7 @@ export class ApPersonService implements OnModuleInit {
|
|||||||
url,
|
url,
|
||||||
fields,
|
fields,
|
||||||
description: _description,
|
description: _description,
|
||||||
|
followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null,
|
||||||
followingVisibility,
|
followingVisibility,
|
||||||
followersVisibility,
|
followersVisibility,
|
||||||
birthday: bday?.[0] ?? null,
|
birthday: bday?.[0] ?? null,
|
||||||
|
@ -13,6 +13,7 @@ export interface IObject {
|
|||||||
name?: string | null;
|
name?: string | null;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
_misskey_summary?: string;
|
_misskey_summary?: string;
|
||||||
|
_misskey_followedMessage?: string | null;
|
||||||
published?: string;
|
published?: string;
|
||||||
cc?: ApObject;
|
cc?: ApObject;
|
||||||
to?: ApObject;
|
to?: ApObject;
|
||||||
|
@ -59,7 +59,7 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
async #packInternal <T extends MiNotification | MiGroupedNotification> (
|
async #packInternal <T extends MiNotification | MiGroupedNotification> (
|
||||||
src: T,
|
src: T,
|
||||||
meId: MiUser['id'],
|
meId: MiUser['id'],
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
options: {
|
options: {
|
||||||
checkValidNotifier?: boolean;
|
checkValidNotifier?: boolean;
|
||||||
},
|
},
|
||||||
@ -159,6 +159,9 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
...(notification.type === 'roleAssigned' ? {
|
...(notification.type === 'roleAssigned' ? {
|
||||||
role: role,
|
role: role,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
...(notification.type === 'followRequestAccepted' ? {
|
||||||
|
message: notification.message,
|
||||||
|
} : {}),
|
||||||
...(notification.type === 'achievementEarned' ? {
|
...(notification.type === 'achievementEarned' ? {
|
||||||
achievement: notification.achievement,
|
achievement: notification.achievement,
|
||||||
} : {}),
|
} : {}),
|
||||||
@ -233,7 +236,7 @@ export class NotificationEntityService implements OnModuleInit {
|
|||||||
public async pack(
|
public async pack(
|
||||||
src: MiNotification | MiGroupedNotification,
|
src: MiNotification | MiGroupedNotification,
|
||||||
meId: MiUser['id'],
|
meId: MiUser['id'],
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
options: {
|
options: {
|
||||||
checkValidNotifier?: boolean;
|
checkValidNotifier?: boolean;
|
||||||
},
|
},
|
||||||
|
@ -508,7 +508,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
name: r.name,
|
name: r.name,
|
||||||
iconUrl: r.iconUrl,
|
iconUrl: r.iconUrl,
|
||||||
displayOrder: r.displayOrder,
|
displayOrder: r.displayOrder,
|
||||||
}))
|
})),
|
||||||
) : undefined,
|
) : undefined,
|
||||||
|
|
||||||
...(isDetailed ? {
|
...(isDetailed ? {
|
||||||
@ -567,6 +567,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
...(isDetailed && isMe ? {
|
...(isDetailed && isMe ? {
|
||||||
avatarId: user.avatarId,
|
avatarId: user.avatarId,
|
||||||
bannerId: user.bannerId,
|
bannerId: user.bannerId,
|
||||||
|
followedMessage: profile!.followedMessage,
|
||||||
isModerator: isModerator,
|
isModerator: isModerator,
|
||||||
isAdmin: isAdmin,
|
isAdmin: isAdmin,
|
||||||
injectFeaturedNote: profile!.injectFeaturedNote,
|
injectFeaturedNote: profile!.injectFeaturedNote,
|
||||||
@ -635,6 +636,7 @@ export class UserEntityService implements OnModuleInit {
|
|||||||
isRenoteMuted: relation.isRenoteMuted,
|
isRenoteMuted: relation.isRenoteMuted,
|
||||||
notify: relation.following?.notify ?? 'none',
|
notify: relation.following?.notify ?? 'none',
|
||||||
withReplies: relation.following?.withReplies ?? false,
|
withReplies: relation.following?.withReplies ?? false,
|
||||||
|
followedMessage: relation.isFollowing ? profile!.followedMessage : undefined,
|
||||||
} : {}),
|
} : {}),
|
||||||
} as Promiseable<Packed<S>>;
|
} as Promiseable<Packed<S>>;
|
||||||
|
|
||||||
|
@ -69,6 +69,7 @@ export type MiNotification = {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
notifierId: MiUser['id'];
|
notifierId: MiUser['id'];
|
||||||
|
message: string | null;
|
||||||
} | {
|
} | {
|
||||||
type: 'roleAssigned';
|
type: 'roleAssigned';
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -289,5 +289,6 @@ export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toStr
|
|||||||
export const passwordSchema = { type: 'string', minLength: 1 } as const;
|
export const passwordSchema = { type: 'string', minLength: 1 } as const;
|
||||||
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||||
export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const;
|
export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const;
|
||||||
|
export const followedMessageSchema = { type: 'string', minLength: 1, maxLength: 256 } as const;
|
||||||
export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||||
export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
|
export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
|
||||||
|
@ -42,6 +42,14 @@ export class MiUserProfile {
|
|||||||
})
|
})
|
||||||
public description: string | null;
|
public description: string | null;
|
||||||
|
|
||||||
|
// フォローされた際のメッセージ
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 256, nullable: true,
|
||||||
|
})
|
||||||
|
public followedMessage: string | null;
|
||||||
|
|
||||||
|
// TODO: 鍵アカウントの場合の、フォローリクエスト受信時のメッセージも設定できるようにする
|
||||||
|
|
||||||
@Column('jsonb', {
|
@Column('jsonb', {
|
||||||
default: [],
|
default: [],
|
||||||
})
|
})
|
||||||
|
@ -267,6 +267,10 @@ export const packedNotificationSchema = {
|
|||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -370,6 +370,10 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||||||
ref: 'RoleLite',
|
ref: 'RoleLite',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
followedMessage: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: true, optional: true,
|
||||||
|
},
|
||||||
memo: {
|
memo: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
@ -437,6 +441,10 @@ export const packedMeDetailedOnlySchema = {
|
|||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
|
followedMessage: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: true, optional: false,
|
||||||
|
},
|
||||||
isModerator: {
|
isModerator: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
|
@ -31,6 +31,10 @@ export const meta = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
followedMessage: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
autoAcceptFollowed: {
|
autoAcceptFollowed: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
@ -226,6 +230,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
return {
|
return {
|
||||||
email: profile.email,
|
email: profile.email,
|
||||||
emailVerified: profile.emailVerified,
|
emailVerified: profile.emailVerified,
|
||||||
|
followedMessage: profile.followedMessage,
|
||||||
autoAcceptFollowed: profile.autoAcceptFollowed,
|
autoAcceptFollowed: profile.autoAcceptFollowed,
|
||||||
noCrawle: profile.noCrawle,
|
noCrawle: profile.noCrawle,
|
||||||
preventAiLearning: profile.preventAiLearning,
|
preventAiLearning: profile.preventAiLearning,
|
||||||
|
@ -13,9 +13,8 @@ import { extractHashtags } from '@/misc/extract-hashtags.js';
|
|||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
|
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
|
||||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||||
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
|
import { birthdaySchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js';
|
||||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||||
import { notificationTypes } from '@/types.js';
|
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import { langmap } from '@/misc/langmap.js';
|
import { langmap } from '@/misc/langmap.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
@ -134,6 +133,7 @@ export const paramDef = {
|
|||||||
properties: {
|
properties: {
|
||||||
name: { ...nameSchema, nullable: true },
|
name: { ...nameSchema, nullable: true },
|
||||||
description: { ...descriptionSchema, nullable: true },
|
description: { ...descriptionSchema, nullable: true },
|
||||||
|
followedMessage: { ...followedMessageSchema, nullable: true },
|
||||||
location: { ...locationSchema, nullable: true },
|
location: { ...locationSchema, nullable: true },
|
||||||
birthday: { ...birthdaySchema, nullable: true },
|
birthday: { ...birthdaySchema, nullable: true },
|
||||||
lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
|
lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
|
||||||
@ -267,6 +267,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ps.description !== undefined) profileUpdates.description = ps.description;
|
if (ps.description !== undefined) profileUpdates.description = ps.description;
|
||||||
|
if (ps.followedMessage !== undefined) profileUpdates.followedMessage = ps.followedMessage;
|
||||||
if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
|
if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
|
||||||
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
||||||
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
||||||
|
@ -7,9 +7,9 @@ process.env.NODE_ENV = 'test';
|
|||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { inspect } from 'node:util';
|
import { inspect } from 'node:util';
|
||||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
|
||||||
import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
|
import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||||
|
|
||||||
describe('ユーザー', () => {
|
describe('ユーザー', () => {
|
||||||
// エンティティとしてのユーザーを主眼においたテストを記述する
|
// エンティティとしてのユーザーを主眼においたテストを記述する
|
||||||
@ -105,6 +105,7 @@ describe('ユーザー', () => {
|
|||||||
isRenoteMuted: user.isRenoteMuted ?? false,
|
isRenoteMuted: user.isRenoteMuted ?? false,
|
||||||
notify: user.notify ?? 'none',
|
notify: user.notify ?? 'none',
|
||||||
withReplies: user.withReplies ?? false,
|
withReplies: user.withReplies ?? false,
|
||||||
|
followedMessage: user.isFollowing ? (user.followedMessage ?? null) : undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -114,6 +115,7 @@ describe('ユーザー', () => {
|
|||||||
...userDetailedNotMe(user),
|
...userDetailedNotMe(user),
|
||||||
avatarId: user.avatarId,
|
avatarId: user.avatarId,
|
||||||
bannerId: user.bannerId,
|
bannerId: user.bannerId,
|
||||||
|
followedMessage: user.followedMessage,
|
||||||
isModerator: user.isModerator,
|
isModerator: user.isModerator,
|
||||||
isAdmin: user.isAdmin,
|
isAdmin: user.isAdmin,
|
||||||
injectFeaturedNote: user.injectFeaturedNote,
|
injectFeaturedNote: user.injectFeaturedNote,
|
||||||
@ -350,6 +352,7 @@ describe('ユーザー', () => {
|
|||||||
// MeDetailedOnly
|
// MeDetailedOnly
|
||||||
assert.strictEqual(response.avatarId, null);
|
assert.strictEqual(response.avatarId, null);
|
||||||
assert.strictEqual(response.bannerId, null);
|
assert.strictEqual(response.bannerId, null);
|
||||||
|
assert.strictEqual(response.followedMessage, null);
|
||||||
assert.strictEqual(response.isModerator, false);
|
assert.strictEqual(response.isModerator, false);
|
||||||
assert.strictEqual(response.isAdmin, false);
|
assert.strictEqual(response.isAdmin, false);
|
||||||
assert.strictEqual(response.injectFeaturedNote, true);
|
assert.strictEqual(response.injectFeaturedNote, true);
|
||||||
@ -413,6 +416,8 @@ describe('ユーザー', () => {
|
|||||||
{ parameters: () => ({ description: 'x'.repeat(1500) }) },
|
{ parameters: () => ({ description: 'x'.repeat(1500) }) },
|
||||||
{ parameters: () => ({ description: 'x' }) },
|
{ parameters: () => ({ description: 'x' }) },
|
||||||
{ parameters: () => ({ description: 'My description' }) },
|
{ parameters: () => ({ description: 'My description' }) },
|
||||||
|
{ parameters: () => ({ followedMessage: null }) },
|
||||||
|
{ parameters: () => ({ followedMessage: 'Thank you' }) },
|
||||||
{ parameters: () => ({ location: null }) },
|
{ parameters: () => ({ location: null }) },
|
||||||
{ parameters: () => ({ location: 'x'.repeat(50) }) },
|
{ parameters: () => ({ location: 'x'.repeat(50) }) },
|
||||||
{ parameters: () => ({ location: 'x' }) },
|
{ parameters: () => ({ location: 'x' }) },
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
accentDarken: ':darken<10<@accent',
|
accentDarken: ':darken<10<@accent',
|
||||||
accentLighten: ':lighten<10<@accent',
|
accentLighten: ':lighten<10<@accent',
|
||||||
accentedBg: ':alpha<0.15<@accent',
|
accentedBg: ':alpha<0.15<@accent',
|
||||||
|
love: '#dd2e44',
|
||||||
focus: ':alpha<0.3<@accent',
|
focus: ':alpha<0.3<@accent',
|
||||||
bg: '#000',
|
bg: '#000',
|
||||||
acrylicBg: ':alpha<0.5<@bg',
|
acrylicBg: ':alpha<0.5<@bg',
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
accentDarken: ':darken<10<@accent',
|
accentDarken: ':darken<10<@accent',
|
||||||
accentLighten: ':lighten<10<@accent',
|
accentLighten: ':lighten<10<@accent',
|
||||||
accentedBg: ':alpha<0.15<@accent',
|
accentedBg: ':alpha<0.15<@accent',
|
||||||
|
love: '#dd2e44',
|
||||||
focus: ':alpha<0.3<@accent',
|
focus: ':alpha<0.3<@accent',
|
||||||
bg: '#fff',
|
bg: '#fff',
|
||||||
acrylicBg: ':alpha<0.5<@bg',
|
acrylicBg: ':alpha<0.5<@bg',
|
||||||
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
|
<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="isFollowing">
|
<template v-else-if="isFollowing">
|
||||||
<span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
|
<span v-if="full" :class="$style.text">{{ i18n.ts.youFollowing }}</span><i class="ti ti-minus"></i>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="!isFollowing && user.isLocked">
|
<template v-else-if="!isFollowing && user.isLocked">
|
||||||
<span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
|
<span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
|
||||||
|
@ -119,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<i class="ti ti-ban"></i>
|
<i class="ti ti-ban"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
|
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
|
||||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i>
|
||||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
||||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||||
<i v-else class="ti ti-plus"></i>
|
<i v-else class="ti ti-plus"></i>
|
||||||
|
@ -128,7 +128,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<i class="ti ti-ban"></i>
|
<i class="ti ti-ban"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
|
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
|
||||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i>
|
||||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
||||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||||
<i v-else class="ti ti-plus"></i>
|
<i v-else class="ti ti-plus"></i>
|
||||||
|
@ -108,7 +108,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template v-else-if="notification.type === 'follow'">
|
<template v-else-if="notification.type === 'follow'">
|
||||||
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
||||||
</template>
|
</template>
|
||||||
<span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
|
<template v-else-if="notification.type === 'followRequestAccepted'">
|
||||||
|
<div :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</div>
|
||||||
|
<div v-if="notification.message" :class="$style.text" style="opacity: 0.6; font-style: oblique;">
|
||||||
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
|
<span>{{ notification.message }}</span>
|
||||||
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template v-else-if="notification.type === 'receiveFollowRequest'">
|
<template v-else-if="notification.type === 'receiveFollowRequest'">
|
||||||
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span>
|
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span>
|
||||||
<div v-if="full && !followRequestDone" :class="$style.followRequestCommands">
|
<div v-if="full && !followRequestDone" :class="$style.followRequestCommands">
|
||||||
@ -211,6 +218,14 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
display: flex;
|
display: flex;
|
||||||
contain: content;
|
contain: content;
|
||||||
|
|
||||||
|
--eventFollow: #36aed2;
|
||||||
|
--eventRenote: #36d298;
|
||||||
|
--eventReply: #007aff;
|
||||||
|
--eventReactionHeart: var(--love);
|
||||||
|
--eventReaction: #e99a0b;
|
||||||
|
--eventAchievement: #cb9a11;
|
||||||
|
--eventOther: #88a6b7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.head {
|
.head {
|
||||||
|
@ -88,14 +88,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
|
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
|
||||||
</FormSlot>
|
</FormSlot>
|
||||||
|
|
||||||
<MkFolder>
|
<MkInput v-model="profile.followedMessage" :max="200" manualSave :mfmPreview="false">
|
||||||
<template #label>{{ i18n.ts.advancedSettings }}</template>
|
<template #label>{{ i18n.ts._profile.followedMessage }}</template>
|
||||||
|
<template #caption>
|
||||||
<div class="_gaps_m">
|
<div>{{ i18n.ts._profile.followedMessageDescription }}</div>
|
||||||
<MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch>
|
<div>{{ i18n.ts._profile.followedMessageDescriptionForLockedAccount }}</div>
|
||||||
<MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch>
|
</template>
|
||||||
</div>
|
</MkInput>
|
||||||
</MkFolder>
|
|
||||||
|
|
||||||
<MkSelect v-model="reactionAcceptance">
|
<MkSelect v-model="reactionAcceptance">
|
||||||
<template #label>{{ i18n.ts.reactionAcceptance }}</template>
|
<template #label>{{ i18n.ts.reactionAcceptance }}</template>
|
||||||
@ -105,6 +104,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<option value="nonSensitiveOnlyForLocalLikeOnlyForRemote">{{ i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }}</option>
|
<option value="nonSensitiveOnlyForLocalLikeOnlyForRemote">{{ i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }}</option>
|
||||||
<option value="likeOnly">{{ i18n.ts.likeOnly }}</option>
|
<option value="likeOnly">{{ i18n.ts.likeOnly }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
|
||||||
|
<MkFolder>
|
||||||
|
<template #label>{{ i18n.ts.advancedSettings }}</template>
|
||||||
|
|
||||||
|
<div class="_gaps_m">
|
||||||
|
<MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch>
|
||||||
|
<MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -138,6 +146,7 @@ const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAccep
|
|||||||
const profile = reactive({
|
const profile = reactive({
|
||||||
name: $i.name,
|
name: $i.name,
|
||||||
description: $i.description,
|
description: $i.description,
|
||||||
|
followedMessage: $i.followedMessage,
|
||||||
location: $i.location,
|
location: $i.location,
|
||||||
birthday: $i.birthday,
|
birthday: $i.birthday,
|
||||||
lang: $i.lang,
|
lang: $i.lang,
|
||||||
@ -185,6 +194,8 @@ function save() {
|
|||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
description: profile.description || null,
|
description: profile.description || null,
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
|
followedMessage: profile.followedMessage || null,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
location: profile.location || null,
|
location: profile.location || null,
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
birthday: profile.birthday || null,
|
birthday: profile.birthday || null,
|
||||||
|
@ -47,6 +47,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
|
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="user.followedMessage != null" class="followedMessage">
|
||||||
|
<div style="border: solid 1px var(--love); border-radius: 6px; background: color-mix(in srgb, var(--love), transparent 90%); padding: 6px 8px;">
|
||||||
|
<Mfm :text="user.followedMessage" :author="user"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="user.roles.length > 0" class="roles">
|
<div v-if="user.roles.length > 0" class="roles">
|
||||||
<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">
|
<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">
|
||||||
<MkA v-adaptive-bg :to="`/roles/${role.id}`">
|
<MkA v-adaptive-bg :to="`/roles/${role.id}`">
|
||||||
@ -460,6 +465,11 @@ onUnmounted(() => {
|
|||||||
box-shadow: 1px 1px 3px rgba(#000, 0.2);
|
box-shadow: 1px 1px 3px rgba(#000, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .followedMessage {
|
||||||
|
padding: 24px 24px 0 154px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
> .roles {
|
> .roles {
|
||||||
padding: 24px 24px 0 154px;
|
padding: 24px 24px 0 154px;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
@ -642,6 +652,10 @@ onUnmounted(() => {
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .followedMessage {
|
||||||
|
padding: 16px 16px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
> .roles {
|
> .roles {
|
||||||
padding: 16px 16px 0 16px;
|
padding: 16px 16px 0 16px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -18,13 +18,6 @@
|
|||||||
--minBottomSpacing: var(--minBottomSpacingMobile);
|
--minBottomSpacing: var(--minBottomSpacingMobile);
|
||||||
|
|
||||||
//--ad: rgb(255 169 0 / 10%);
|
//--ad: rgb(255 169 0 / 10%);
|
||||||
--eventFollow: #36aed2;
|
|
||||||
--eventRenote: #36d298;
|
|
||||||
--eventReply: #007aff;
|
|
||||||
--eventReactionHeart: #dd2e44;
|
|
||||||
--eventReaction: #e99a0b;
|
|
||||||
--eventAchievement: #cb9a11;
|
|
||||||
--eventOther: #88a6b7;
|
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
--margin: var(--marginHalf);
|
--margin: var(--marginHalf);
|
||||||
|
@ -3789,6 +3789,7 @@ export type components = {
|
|||||||
/** @default false */
|
/** @default false */
|
||||||
securityKeys: boolean;
|
securityKeys: boolean;
|
||||||
roles: components['schemas']['RoleLite'][];
|
roles: components['schemas']['RoleLite'][];
|
||||||
|
followedMessage?: string | null;
|
||||||
memo: string | null;
|
memo: string | null;
|
||||||
moderationNote?: string;
|
moderationNote?: string;
|
||||||
isFollowing?: boolean;
|
isFollowing?: boolean;
|
||||||
@ -3808,6 +3809,7 @@ export type components = {
|
|||||||
avatarId: string | null;
|
avatarId: string | null;
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
bannerId: string | null;
|
bannerId: string | null;
|
||||||
|
followedMessage: string | null;
|
||||||
isModerator: boolean | null;
|
isModerator: boolean | null;
|
||||||
isAdmin: boolean | null;
|
isAdmin: boolean | null;
|
||||||
injectFeaturedNote: boolean;
|
injectFeaturedNote: boolean;
|
||||||
@ -4247,7 +4249,7 @@ export type components = {
|
|||||||
user: components['schemas']['UserLite'];
|
user: components['schemas']['UserLite'];
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
userId: string;
|
userId: string;
|
||||||
} | {
|
} | ({
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
id: string;
|
id: string;
|
||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
@ -4257,7 +4259,8 @@ export type components = {
|
|||||||
user: components['schemas']['UserLite'];
|
user: components['schemas']['UserLite'];
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
userId: string;
|
userId: string;
|
||||||
} | {
|
message: string | null;
|
||||||
|
}) | {
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
id: string;
|
id: string;
|
||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
@ -8935,6 +8938,7 @@ export type operations = {
|
|||||||
'application/json': {
|
'application/json': {
|
||||||
email: string | null;
|
email: string | null;
|
||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
|
followedMessage: string | null;
|
||||||
autoAcceptFollowed: boolean;
|
autoAcceptFollowed: boolean;
|
||||||
noCrawle: boolean;
|
noCrawle: boolean;
|
||||||
preventAiLearning: boolean;
|
preventAiLearning: boolean;
|
||||||
@ -19663,6 +19667,7 @@ export type operations = {
|
|||||||
'application/json': {
|
'application/json': {
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
|
followedMessage?: string | null;
|
||||||
location?: string | null;
|
location?: string | null;
|
||||||
birthday?: string | null;
|
birthday?: string | null;
|
||||||
/** @enum {string|null} */
|
/** @enum {string|null} */
|
||||||
|
Loading…
Reference in New Issue
Block a user