From 4f652dab022a68be31c1eac67bd58ba164ddacca Mon Sep 17 00:00:00 2001 From: ThatOneCalculator Date: Mon, 23 May 2022 18:13:49 -0700 Subject: [PATCH 1/5] feat: :sparkles: Start work on pronouns --- locales/en-US.yml | 1 + locales/ja-JP.yml | 3 +- .../src/models/entities/user-profile.ts | 146 ++++++++++-------- .../backend/src/models/repositories/user.ts | 6 +- packages/backend/src/models/schema/user.ts | 5 + .../src/remote/activitypub/models/person.ts | 2 + .../src/remote/activitypub/renderer/person.ts | 4 + .../backend/src/remote/activitypub/type.ts | 1 + .../src/server/api/endpoints/i/update.ts | 2 + .../client/src/pages/settings/profile.vue | 7 + packages/client/src/pages/user/index.vue | 4 + 11 files changed, 116 insertions(+), 65 deletions(-) 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 }}
From 717735421945b39df76a4b0dae8031452485922e Mon Sep 17 00:00:00 2001 From: ThatOneCalculator Date: Mon, 23 May 2022 18:18:22 -0700 Subject: [PATCH 2/5] Undo IDE formatting --- locales/ja-JP.yml | 2 +- .../src/models/entities/user-profile.ts | 152 ++++++++---------- 2 files changed, 70 insertions(+), 84 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 39b3d9a3c..563ad5c95 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1640,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 24498f807..0f923bffb 100644 --- a/packages/backend/src/models/entities/user-profile.ts +++ b/packages/backend/src/models/entities/user-profile.ts @@ -9,43 +9,39 @@ 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.", - }) - public location: string | null; - - @Column("varchar", { - length: 20, - nullable: true, - comment: "The pronouns of the User.", + @Column('varchar', { + length: 32, 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.", + @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.', }) 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: { @@ -53,129 +49,121 @@ 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; @@ -184,38 +172,37 @@ 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: [], @@ -224,10 +211,9 @@ 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 From 446ab176ac8af4711488a6a46dc2067c79dcf3e8 Mon Sep 17 00:00:00 2001 From: ThatOneCalculator Date: Mon, 23 May 2022 18:34:43 -0700 Subject: [PATCH 3/5] =?UTF-8?q?docs:=20=F0=9F=93=9D=20Add=20to=20CHANGELOG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21ae948d0..294461c4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ You should also include the user name that made the change. Your own theme color may be unset if it was in an invalid format. Admins should check their instance settings if in doubt. - Perform port diagnosis at startup only when Listen fails @mei23 +- Add pronouns to vCard and display when replying @ThatOneCalculator ### Bugfixes - Client: fix settings page @tamaina From 82eb927dd48449742d3a966d3e3f5f8e571b701c Mon Sep 17 00:00:00 2001 From: ThatOneCalculator Date: Mon, 23 May 2022 19:03:29 -0700 Subject: [PATCH 4/5] refactor: :recycle: Use vcard:Gender for vCard https://datatracker.ietf.org/doc/html/rfc6350#section-6.2.7 --- packages/backend/src/remote/activitypub/models/person.ts | 4 ++-- packages/backend/src/remote/activitypub/renderer/person.ts | 2 +- packages/backend/src/remote/activitypub/type.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index c5955f319..85ec4872c 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -192,7 +192,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise Date: Mon, 23 May 2022 19:07:39 -0700 Subject: [PATCH 5/5] refactor: :recycle: Use `vcard:Nickname` instead of `vcard:Gender` --- packages/backend/src/remote/activitypub/models/person.ts | 4 ++-- packages/backend/src/remote/activitypub/renderer/person.ts | 2 +- packages/backend/src/remote/activitypub/type.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 85ec4872c..8f37ba599 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -192,7 +192,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise