diff --git a/locales/en-US.yml b/locales/en-US.yml index a7d69d6d4..f542512f1 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -258,6 +258,7 @@ remoteUserCaution: "As this user is from a remote instance, the shown informatio activity: "Activity" images: "Images" birthday: "Birthday" +pronouns: "Pronouns" yearsOld: "{age} years old" registeredDate: "Joined on" location: "Location" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index f64246d15..39b3d9a3c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -258,6 +258,7 @@ remoteUserCaution: "リモートユーザーのため、情報が不完全です activity: "アクティビティ" images: "画像" birthday: "誕生日" +pronouns: "代名詞" yearsOld: "{age}歳" registeredDate: "登録日" location: "場所" @@ -1639,7 +1640,7 @@ _pages: _for: arg1: "回数" arg2: "処理" - typeError: "スロット{slot}は\"{expect}\"を受け付けますが、\"{actual}\"が入れられています!" + typeError: 'スロット{slot}は"{expect}"を受け付けますが、"{actual}"が入れられています!' thereIsEmptySlot: "スロット{slot}が空です!" types: string: "テキスト" diff --git a/packages/backend/src/models/entities/user-profile.ts b/packages/backend/src/models/entities/user-profile.ts index 1778742ea..24498f807 100644 --- a/packages/backend/src/models/entities/user-profile.ts +++ b/packages/backend/src/models/entities/user-profile.ts @@ -9,33 +9,43 @@ import { ffVisibility, notificationTypes } from '@/types.js'; @Entity() export class UserProfile { @PrimaryColumn(id()) - public userId: User['id']; + public userId: User["id"]; - @OneToOne(type => User, { - onDelete: 'CASCADE', + @OneToOne((type) => User, { + onDelete: "CASCADE", }) @JoinColumn() public user: User | null; - @Column('varchar', { - length: 128, nullable: true, - comment: 'The location of the User.', + @Column("varchar", { + length: 128, + nullable: true, + comment: "The location of the User.", }) public location: string | null; - @Column('char', { - length: 10, nullable: true, - comment: 'The birthday (YYYY-MM-DD) of the User.', + @Column("varchar", { + length: 20, + nullable: true, + comment: "The pronouns of the User.", + }) + public pronouns: string | null; + + @Column("char", { + length: 10, + nullable: true, + comment: "The birthday (YYYY-MM-DD) of the User.", }) public birthday: string | null; - @Column('varchar', { - length: 2048, nullable: true, - comment: 'The description (bio) of the User.', + @Column("varchar", { + length: 2048, + nullable: true, + comment: "The description (bio) of the User.", }) public description: string | null; - @Column('jsonb', { + @Column("jsonb", { default: [], }) public fields: { @@ -43,121 +53,129 @@ export class UserProfile { value: string; }[]; - @Column('varchar', { - length: 32, nullable: true, + @Column("varchar", { + length: 32, + nullable: true, }) public lang: string | null; - @Column('varchar', { - length: 512, nullable: true, - comment: 'Remote URL of the user.', + @Column("varchar", { + length: 512, + nullable: true, + comment: "Remote URL of the user.", }) public url: string | null; - @Column('varchar', { - length: 128, nullable: true, - comment: 'The email address of the User.', + @Column("varchar", { + length: 128, + nullable: true, + comment: "The email address of the User.", }) public email: string | null; - @Column('varchar', { - length: 128, nullable: true, + @Column("varchar", { + length: 128, + nullable: true, }) public emailVerifyCode: string | null; - @Column('boolean', { + @Column("boolean", { default: false, }) public emailVerified: boolean; - @Column('jsonb', { - default: ['follow', 'receiveFollowRequest', 'groupInvited'], + @Column("jsonb", { + default: ["follow", "receiveFollowRequest", "groupInvited"], }) public emailNotificationTypes: string[]; - @Column('boolean', { + @Column("boolean", { default: false, }) public publicReactions: boolean; - @Column('enum', { + @Column("enum", { enum: ffVisibility, - default: 'public', + default: "public", }) public ffVisibility: typeof ffVisibility[number]; - @Column('varchar', { - length: 128, nullable: true, + @Column("varchar", { + length: 128, + nullable: true, }) public twoFactorTempSecret: string | null; - @Column('varchar', { - length: 128, nullable: true, + @Column("varchar", { + length: 128, + nullable: true, }) public twoFactorSecret: string | null; - @Column('boolean', { + @Column("boolean", { default: false, }) public twoFactorEnabled: boolean; - @Column('boolean', { + @Column("boolean", { default: false, }) public securityKeysAvailable: boolean; - @Column('boolean', { + @Column("boolean", { default: false, }) public usePasswordLessLogin: boolean; - @Column('varchar', { - length: 128, nullable: true, - comment: 'The password hash of the User. It will be null if the origin of the user is local.', + @Column("varchar", { + length: 128, + nullable: true, + comment: + "The password hash of the User. It will be null if the origin of the user is local.", }) public password: string | null; // TODO: そのうち消す - @Column('jsonb', { + @Column("jsonb", { default: {}, - comment: 'The client-specific data of the User.', + comment: "The client-specific data of the User.", }) public clientData: Record; // TODO: そのうち消す - @Column('jsonb', { + @Column("jsonb", { default: {}, - comment: 'The room data of the User.', + comment: "The room data of the User.", }) public room: Record; - @Column('boolean', { + @Column("boolean", { default: false, }) public autoAcceptFollowed: boolean; - @Column('boolean', { + @Column("boolean", { default: false, - comment: 'Whether reject index by crawler.', + comment: "Whether reject index by crawler.", }) public noCrawle: boolean; - @Column('boolean', { + @Column("boolean", { default: false, }) public alwaysMarkNsfw: boolean; - @Column('boolean', { + @Column("boolean", { default: false, }) public carefulBot: boolean; - @Column('boolean', { + @Column("boolean", { default: true, }) public injectFeaturedNote: boolean; - @Column('boolean', { + @Column("boolean", { default: true, }) public receiveAnnouncementEmail: boolean; @@ -166,37 +184,38 @@ export class UserProfile { ...id(), nullable: true, }) - public pinnedPageId: Page['id'] | null; + public pinnedPageId: Page["id"] | null; - @OneToOne(type => Page, { - onDelete: 'SET NULL', + @OneToOne((type) => Page, { + onDelete: "SET NULL", }) @JoinColumn() public pinnedPage: Page | null; - @Column('jsonb', { + @Column("jsonb", { default: {}, }) public integrations: Record; @Index() - @Column('boolean', { - default: false, select: false, + @Column("boolean", { + default: false, + select: false, }) public enableWordMute: boolean; - @Column('jsonb', { + @Column("jsonb", { default: [], }) public mutedWords: string[][]; - @Column('jsonb', { + @Column("jsonb", { default: [], - comment: 'List of instances muted by the user.', + comment: "List of instances muted by the user.", }) public mutedInstances: string[]; - @Column('enum', { + @Column("enum", { enum: notificationTypes, array: true, default: [], @@ -205,9 +224,10 @@ export class UserProfile { //#region Denormalized fields @Index() - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', + @Column("varchar", { + length: 128, + nullable: true, + comment: "[Denormalized]", }) public userHost: string | null; //#endregion diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 541fbaf00..a6404245e 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -16,7 +16,7 @@ const userInstanceCache = new Cache(1000 * 60 * 60 * 3); type IsUserDetailed = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>; type IsMeAndIsUserDetailed = - Detailed extends true ? + Detailed extends true ? ExpectsMe extends true ? Packed<'MeDetailed'> : ExpectsMe extends false ? Packed<'UserDetailedNotMe'> : Packed<'UserDetailed'> : @@ -30,6 +30,7 @@ const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; const descriptionSchema = { type: 'string', minLength: 1, maxLength: 500 } as const; const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const; +const pronounsSchema = { type: 'string', minLength: 1, maxLength: 20 } as const; function isLocalUser(user: User): user is ILocalUser; function isLocalUser(user: T): user is T & { host: null; }; @@ -50,6 +51,7 @@ export const UserRepository = db.getRepository(User).extend({ descriptionSchema, locationSchema, birthdaySchema, + pronounsSchema, //#region Validators validateLocalUsername: ajv.compile(localUsernameSchema), @@ -58,6 +60,7 @@ export const UserRepository = db.getRepository(User).extend({ validateDescription: ajv.compile(descriptionSchema), validateLocation: ajv.compile(locationSchema), validateBirthday: ajv.compile(birthdaySchema), + validatePronouns: ajv.compile(pronounsSchema), //#endregion async getRelation(me: User['id'], target: User['id']) { @@ -318,6 +321,7 @@ export const UserRepository = db.getRepository(User).extend({ isSilenced: user.isSilenced || falsy, isSuspended: user.isSuspended || falsy, description: profile!.description, + pronouns: profile!.pronouns, location: profile!.location, birthday: profile!.birthday, lang: profile!.lang, diff --git a/packages/backend/src/models/schema/user.ts b/packages/backend/src/models/schema/user.ts index 253681695..97fe79fe0 100644 --- a/packages/backend/src/models/schema/user.ts +++ b/packages/backend/src/models/schema/user.ts @@ -143,6 +143,11 @@ export const packedUserDetailedNotMeOnlySchema = { nullable: true, optional: false, example: 'Hi masters, I am Ai!', }, + pronouns: { + type: 'string', + nullable: true, optional: false, + example: 'They/Them', + }, location: { type: 'string', nullable: true, optional: false, diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 6097e3b6e..c5955f319 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -192,6 +192,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise { if (ps.name !== undefined) updates.name = ps.name; if (ps.description !== undefined) profileUpdates.description = ps.description; if (ps.lang !== undefined) profileUpdates.lang = ps.lang; + if (ps.pronouns !== undefined) profileUpdates.pronouns = ps.pronouns; if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue index e991d725b..c01ed4396 100644 --- a/packages/client/src/pages/settings/profile.vue +++ b/packages/client/src/pages/settings/profile.vue @@ -17,6 +17,11 @@ + + + + + @@ -82,6 +87,7 @@ import { langmap } from '@/scripts/langmap'; const profile = reactive({ name: $i.name, description: $i.description, + pronouns: $i.pronouns, location: $i.location, birthday: $i.birthday, lang: $i.lang, @@ -120,6 +126,7 @@ function save() { os.apiWithDialog('i/update', { name: profile.name || null, description: profile.description || null, + pronouns: profile.pronouns || null, location: profile.location || null, birthday: profile.birthday || null, lang: profile.lang || null, diff --git a/packages/client/src/pages/user/index.vue b/packages/client/src/pages/user/index.vue index 17e815892..d3ff3aef2 100644 --- a/packages/client/src/pages/user/index.vue +++ b/packages/client/src/pages/user/index.vue @@ -47,6 +47,10 @@

{{ $ts.noAccountDescription }}

+
+
{{ $ts.pronouns }}
+
{{ user.pronouns }}
+
{{ $ts.location }}
{{ user.location }}