feat: ✨ Start work on pronouns
This commit is contained in:
		
							parent
							
								
									f90c947036
								
							
						
					
					
						commit
						4f652dab02
					
				
					 11 changed files with 116 additions and 65 deletions
				
			
		|  | @ -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" | ||||
|  |  | |||
|  | @ -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: "テキスト" | ||||
|  |  | |||
|  | @ -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<string, any>; | ||||
| 
 | ||||
| 	// TODO: そのうち消す
 | ||||
| 	@Column('jsonb', { | ||||
| 	@Column("jsonb", { | ||||
| 		default: {}, | ||||
| 		comment: 'The room data of the User.', | ||||
| 		comment: "The room data of the User.", | ||||
| 	}) | ||||
| 	public room: Record<string, any>; | ||||
| 
 | ||||
| 	@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<string, any>; | ||||
| 
 | ||||
| 	@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
 | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ const userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3); | |||
| 
 | ||||
| type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>; | ||||
| type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> = | ||||
| 	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<T extends { host: User['host'] }>(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, | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -192,6 +192,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us | |||
| 				description: person.summary ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, | ||||
| 				url: getOneApHrefNullable(person.url), | ||||
| 				fields, | ||||
| 				pronouns: person['vcard:Pronouns'] || null, | ||||
| 				birthday: bday ? bday[0] : null, | ||||
| 				location: person['vcard:Address'] || null, | ||||
| 				userHost: host, | ||||
|  | @ -368,6 +369,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint | |||
| 		url: getOneApHrefNullable(person.url), | ||||
| 		fields, | ||||
| 		description: person.summary ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, | ||||
| 		pronouns: person['vcard:Pronouns'] || null, | ||||
| 		birthday: bday ? bday[0] : null, | ||||
| 		location: person['vcard:Address'] || null, | ||||
| 	}); | ||||
|  |  | |||
|  | @ -77,6 +77,10 @@ export async function renderPerson(user: ILocalUser) { | |||
| 		attachment: attachment.length ? attachment : undefined, | ||||
| 	} as any; | ||||
| 
 | ||||
| 	if (profile?.pronouns) { | ||||
| 		person['vcard:Pronouns'] = profile.pronouns; | ||||
| 	} | ||||
| 
 | ||||
| 	if (profile?.birthday) { | ||||
| 		person['vcard:bday'] = profile.birthday; | ||||
| 	} | ||||
|  |  | |||
|  | @ -164,6 +164,7 @@ export interface IActor extends IObject { | |||
| 	endpoints?: { | ||||
| 		sharedInbox?: string; | ||||
| 	}; | ||||
| 	'vcard:Pronouns'?: string; | ||||
| 	'vcard:bday'?: string; | ||||
| 	'vcard:Address'?: string; | ||||
| } | ||||
|  |  | |||
|  | @ -72,6 +72,7 @@ export const paramDef = { | |||
| 	properties: { | ||||
| 		name: { ...Users.nameSchema, nullable: true }, | ||||
| 		description: { ...Users.descriptionSchema, nullable: true }, | ||||
| 		pronouns: { ...Users.pronounsSchema, nullable: true }, | ||||
| 		location: { ...Users.locationSchema, nullable: true }, | ||||
| 		birthday: { ...Users.birthdaySchema, nullable: true }, | ||||
| 		lang: { type: 'string', enum: [null, ...Object.keys(langmap)], nullable: true }, | ||||
|  | @ -132,6 +133,7 @@ export default define(meta, paramDef, async (ps, _user, token) => { | |||
| 	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; | ||||
|  |  | |||
|  | @ -17,6 +17,11 @@ | |||
| 		<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template> | ||||
| 	</FormTextarea> | ||||
| 
 | ||||
| 	<FormInput v-model="profile.pronouns" manual-save class="_formBlock"> | ||||
| 		<template #label>{{ i18n.ts.pronouns }}</template> | ||||
| 		<template #prefix><i class="fas fa-heart"></i></template> | ||||
| 	</FormInput> | ||||
| 
 | ||||
| 	<FormInput v-model="profile.location" manual-save class="_formBlock"> | ||||
| 		<template #label>{{ i18n.ts.location }}</template> | ||||
| 		<template #prefix><i class="fas fa-map-marker-alt"></i></template> | ||||
|  | @ -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, | ||||
|  |  | |||
|  | @ -47,6 +47,10 @@ | |||
| 								<p v-else class="empty">{{ $ts.noAccountDescription }}</p> | ||||
| 							</div> | ||||
| 							<div class="fields system"> | ||||
| 								<dl v-if="user.pronouns" class="field"> | ||||
| 									<dt class="name"><i class="fas fa-heart fa-fw"></i> {{ $ts.pronouns }}</dt> | ||||
| 									<dd class="value">{{ user.pronouns }}</dd> | ||||
| 								</dl> | ||||
| 								<dl v-if="user.location" class="field"> | ||||
| 									<dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt> | ||||
| 									<dd class="value">{{ user.location }}</dd> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue