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:
syuilo 2024-09-28 09:55:21 +09:00 committed by GitHub
parent e4d4cc5277
commit 28e9d4e483
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 156 additions and 38 deletions

View file

@ -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
View file

@ -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": {
/** /**

View file

@ -2297,6 +2297,9 @@ _profile:
changeBanner: "バナー画像を変更" changeBanner: "バナー画像を変更"
verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。" verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。"
avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。" avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。"
followedMessage: "フォローされた時のメッセージ"
followedMessageDescription: "フォローされた時に相手に表示するメッセージを設定できます。"
followedMessageDescriptionForLockedAccount: "フォローを承認制にしている場合、フォローリクエストを許可した時に表示されます。"
_exportOrImport: _exportOrImport:
allNotes: "全てのノート" allNotes: "全てのノート"

View 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"');
}
}

View file

@ -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([

View file

@ -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,

View file

@ -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#',

View file

@ -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,

View file

@ -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;

View file

@ -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;
}, },

View file

@ -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>>;

View file

@ -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;

View file

@ -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;

View file

@ -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: [],
}) })

View file

@ -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',

View file

@ -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,

View file

@ -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,

View file

@ -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;

View file

@ -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' }) },

View file

@ -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',

View file

@ -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',

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 {

View file

@ -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,

View file

@ -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;

View file

@ -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);

View file

@ -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} */