feat: Start work on pronouns

This commit is contained in:
ThatOneCalculator 2022-05-23 18:13:49 -07:00
parent f90c947036
commit 4f652dab02
11 changed files with 116 additions and 65 deletions

View file

@ -258,6 +258,7 @@ remoteUserCaution: "As this user is from a remote instance, the shown informatio
activity: "Activity" activity: "Activity"
images: "Images" images: "Images"
birthday: "Birthday" birthday: "Birthday"
pronouns: "Pronouns"
yearsOld: "{age} years old" yearsOld: "{age} years old"
registeredDate: "Joined on" registeredDate: "Joined on"
location: "Location" location: "Location"

View file

@ -258,6 +258,7 @@ remoteUserCaution: "リモートユーザーのため、情報が不完全です
activity: "アクティビティ" activity: "アクティビティ"
images: "画像" images: "画像"
birthday: "誕生日" birthday: "誕生日"
pronouns: "代名詞"
yearsOld: "{age}歳" yearsOld: "{age}歳"
registeredDate: "登録日" registeredDate: "登録日"
location: "場所" location: "場所"
@ -1639,7 +1640,7 @@ _pages:
_for: _for:
arg1: "回数" arg1: "回数"
arg2: "処理" arg2: "処理"
typeError: "スロット{slot}は\"{expect}\"を受け付けますが、\"{actual}\"が入れられています!" typeError: 'スロット{slot}は"{expect}"を受け付けますが、"{actual}"が入れられています!'
thereIsEmptySlot: "スロット{slot}が空です!" thereIsEmptySlot: "スロット{slot}が空です!"
types: types:
string: "テキスト" string: "テキスト"

View file

@ -9,33 +9,43 @@ import { ffVisibility, notificationTypes } from '@/types.js';
@Entity() @Entity()
export class UserProfile { export class UserProfile {
@PrimaryColumn(id()) @PrimaryColumn(id())
public userId: User['id']; public userId: User["id"];
@OneToOne(type => User, { @OneToOne((type) => User, {
onDelete: 'CASCADE', onDelete: "CASCADE",
}) })
@JoinColumn() @JoinColumn()
public user: User | null; public user: User | null;
@Column('varchar', { @Column("varchar", {
length: 128, nullable: true, length: 128,
comment: 'The location of the User.', nullable: true,
comment: "The location of the User.",
}) })
public location: string | null; public location: string | null;
@Column('char', { @Column("varchar", {
length: 10, nullable: true, length: 20,
comment: 'The birthday (YYYY-MM-DD) of the User.', 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; public birthday: string | null;
@Column('varchar', { @Column("varchar", {
length: 2048, nullable: true, length: 2048,
comment: 'The description (bio) of the User.', nullable: true,
comment: "The description (bio) of the User.",
}) })
public description: string | null; public description: string | null;
@Column('jsonb', { @Column("jsonb", {
default: [], default: [],
}) })
public fields: { public fields: {
@ -43,121 +53,129 @@ export class UserProfile {
value: string; value: string;
}[]; }[];
@Column('varchar', { @Column("varchar", {
length: 32, nullable: true, length: 32,
nullable: true,
}) })
public lang: string | null; public lang: string | null;
@Column('varchar', { @Column("varchar", {
length: 512, nullable: true, length: 512,
comment: 'Remote URL of the user.', nullable: true,
comment: "Remote URL of the user.",
}) })
public url: string | null; public url: string | null;
@Column('varchar', { @Column("varchar", {
length: 128, nullable: true, length: 128,
comment: 'The email address of the User.', nullable: true,
comment: "The email address of the User.",
}) })
public email: string | null; public email: string | null;
@Column('varchar', { @Column("varchar", {
length: 128, nullable: true, length: 128,
nullable: true,
}) })
public emailVerifyCode: string | null; public emailVerifyCode: string | null;
@Column('boolean', { @Column("boolean", {
default: false, default: false,
}) })
public emailVerified: boolean; public emailVerified: boolean;
@Column('jsonb', { @Column("jsonb", {
default: ['follow', 'receiveFollowRequest', 'groupInvited'], default: ["follow", "receiveFollowRequest", "groupInvited"],
}) })
public emailNotificationTypes: string[]; public emailNotificationTypes: string[];
@Column('boolean', { @Column("boolean", {
default: false, default: false,
}) })
public publicReactions: boolean; public publicReactions: boolean;
@Column('enum', { @Column("enum", {
enum: ffVisibility, enum: ffVisibility,
default: 'public', default: "public",
}) })
public ffVisibility: typeof ffVisibility[number]; public ffVisibility: typeof ffVisibility[number];
@Column('varchar', { @Column("varchar", {
length: 128, nullable: true, length: 128,
nullable: true,
}) })
public twoFactorTempSecret: string | null; public twoFactorTempSecret: string | null;
@Column('varchar', { @Column("varchar", {
length: 128, nullable: true, length: 128,
nullable: true,
}) })
public twoFactorSecret: string | null; public twoFactorSecret: string | null;
@Column('boolean', { @Column("boolean", {
default: false, default: false,
}) })
public twoFactorEnabled: boolean; public twoFactorEnabled: boolean;
@Column('boolean', { @Column("boolean", {
default: false, default: false,
}) })
public securityKeysAvailable: boolean; public securityKeysAvailable: boolean;
@Column('boolean', { @Column("boolean", {
default: false, default: false,
}) })
public usePasswordLessLogin: boolean; public usePasswordLessLogin: boolean;
@Column('varchar', { @Column("varchar", {
length: 128, nullable: true, length: 128,
comment: 'The password hash of the User. It will be null if the origin of the user is local.', 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; public password: string | null;
// TODO: そのうち消す // TODO: そのうち消す
@Column('jsonb', { @Column("jsonb", {
default: {}, default: {},
comment: 'The client-specific data of the User.', comment: "The client-specific data of the User.",
}) })
public clientData: Record<string, any>; public clientData: Record<string, any>;
// TODO: そのうち消す // TODO: そのうち消す
@Column('jsonb', { @Column("jsonb", {
default: {}, default: {},
comment: 'The room data of the User.', comment: "The room data of the User.",
}) })
public room: Record<string, any>; public room: Record<string, any>;
@Column('boolean', { @Column("boolean", {
default: false, default: false,
}) })
public autoAcceptFollowed: boolean; public autoAcceptFollowed: boolean;
@Column('boolean', { @Column("boolean", {
default: false, default: false,
comment: 'Whether reject index by crawler.', comment: "Whether reject index by crawler.",
}) })
public noCrawle: boolean; public noCrawle: boolean;
@Column('boolean', { @Column("boolean", {
default: false, default: false,
}) })
public alwaysMarkNsfw: boolean; public alwaysMarkNsfw: boolean;
@Column('boolean', { @Column("boolean", {
default: false, default: false,
}) })
public carefulBot: boolean; public carefulBot: boolean;
@Column('boolean', { @Column("boolean", {
default: true, default: true,
}) })
public injectFeaturedNote: boolean; public injectFeaturedNote: boolean;
@Column('boolean', { @Column("boolean", {
default: true, default: true,
}) })
public receiveAnnouncementEmail: boolean; public receiveAnnouncementEmail: boolean;
@ -166,37 +184,38 @@ export class UserProfile {
...id(), ...id(),
nullable: true, nullable: true,
}) })
public pinnedPageId: Page['id'] | null; public pinnedPageId: Page["id"] | null;
@OneToOne(type => Page, { @OneToOne((type) => Page, {
onDelete: 'SET NULL', onDelete: "SET NULL",
}) })
@JoinColumn() @JoinColumn()
public pinnedPage: Page | null; public pinnedPage: Page | null;
@Column('jsonb', { @Column("jsonb", {
default: {}, default: {},
}) })
public integrations: Record<string, any>; public integrations: Record<string, any>;
@Index() @Index()
@Column('boolean', { @Column("boolean", {
default: false, select: false, default: false,
select: false,
}) })
public enableWordMute: boolean; public enableWordMute: boolean;
@Column('jsonb', { @Column("jsonb", {
default: [], default: [],
}) })
public mutedWords: string[][]; public mutedWords: string[][];
@Column('jsonb', { @Column("jsonb", {
default: [], default: [],
comment: 'List of instances muted by the user.', comment: "List of instances muted by the user.",
}) })
public mutedInstances: string[]; public mutedInstances: string[];
@Column('enum', { @Column("enum", {
enum: notificationTypes, enum: notificationTypes,
array: true, array: true,
default: [], default: [],
@ -205,9 +224,10 @@ export class UserProfile {
//#region Denormalized fields //#region Denormalized fields
@Index() @Index()
@Column('varchar', { @Column("varchar", {
length: 128, nullable: true, length: 128,
comment: '[Denormalized]', nullable: true,
comment: "[Denormalized]",
}) })
public userHost: string | null; public userHost: string | null;
//#endregion //#endregion

View file

@ -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 descriptionSchema = { type: 'string', minLength: 1, maxLength: 500 } as const;
const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } 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 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: User): user is ILocalUser;
function isLocalUser<T extends { host: User['host'] }>(user: T): user is T & { host: null; }; 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, descriptionSchema,
locationSchema, locationSchema,
birthdaySchema, birthdaySchema,
pronounsSchema,
//#region Validators //#region Validators
validateLocalUsername: ajv.compile(localUsernameSchema), validateLocalUsername: ajv.compile(localUsernameSchema),
@ -58,6 +60,7 @@ export const UserRepository = db.getRepository(User).extend({
validateDescription: ajv.compile(descriptionSchema), validateDescription: ajv.compile(descriptionSchema),
validateLocation: ajv.compile(locationSchema), validateLocation: ajv.compile(locationSchema),
validateBirthday: ajv.compile(birthdaySchema), validateBirthday: ajv.compile(birthdaySchema),
validatePronouns: ajv.compile(pronounsSchema),
//#endregion //#endregion
async getRelation(me: User['id'], target: User['id']) { async getRelation(me: User['id'], target: User['id']) {
@ -318,6 +321,7 @@ export const UserRepository = db.getRepository(User).extend({
isSilenced: user.isSilenced || falsy, isSilenced: user.isSilenced || falsy,
isSuspended: user.isSuspended || falsy, isSuspended: user.isSuspended || falsy,
description: profile!.description, description: profile!.description,
pronouns: profile!.pronouns,
location: profile!.location, location: profile!.location,
birthday: profile!.birthday, birthday: profile!.birthday,
lang: profile!.lang, lang: profile!.lang,

View file

@ -143,6 +143,11 @@ export const packedUserDetailedNotMeOnlySchema = {
nullable: true, optional: false, nullable: true, optional: false,
example: 'Hi masters, I am Ai!', example: 'Hi masters, I am Ai!',
}, },
pronouns: {
type: 'string',
nullable: true, optional: false,
example: 'They/Them',
},
location: { location: {
type: 'string', type: 'string',
nullable: true, optional: false, nullable: true, optional: false,

View file

@ -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, description: person.summary ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
url: getOneApHrefNullable(person.url), url: getOneApHrefNullable(person.url),
fields, fields,
pronouns: person['vcard:Pronouns'] || null,
birthday: bday ? bday[0] : null, birthday: bday ? bday[0] : null,
location: person['vcard:Address'] || null, location: person['vcard:Address'] || null,
userHost: host, userHost: host,
@ -368,6 +369,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
url: getOneApHrefNullable(person.url), url: getOneApHrefNullable(person.url),
fields, fields,
description: person.summary ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null, description: person.summary ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
pronouns: person['vcard:Pronouns'] || null,
birthday: bday ? bday[0] : null, birthday: bday ? bday[0] : null,
location: person['vcard:Address'] || null, location: person['vcard:Address'] || null,
}); });

View file

@ -77,6 +77,10 @@ export async function renderPerson(user: ILocalUser) {
attachment: attachment.length ? attachment : undefined, attachment: attachment.length ? attachment : undefined,
} as any; } as any;
if (profile?.pronouns) {
person['vcard:Pronouns'] = profile.pronouns;
}
if (profile?.birthday) { if (profile?.birthday) {
person['vcard:bday'] = profile.birthday; person['vcard:bday'] = profile.birthday;
} }

View file

@ -164,6 +164,7 @@ export interface IActor extends IObject {
endpoints?: { endpoints?: {
sharedInbox?: string; sharedInbox?: string;
}; };
'vcard:Pronouns'?: string;
'vcard:bday'?: string; 'vcard:bday'?: string;
'vcard:Address'?: string; 'vcard:Address'?: string;
} }

View file

@ -72,6 +72,7 @@ export const paramDef = {
properties: { properties: {
name: { ...Users.nameSchema, nullable: true }, name: { ...Users.nameSchema, nullable: true },
description: { ...Users.descriptionSchema, nullable: true }, description: { ...Users.descriptionSchema, nullable: true },
pronouns: { ...Users.pronounsSchema, nullable: true },
location: { ...Users.locationSchema, nullable: true }, location: { ...Users.locationSchema, nullable: true },
birthday: { ...Users.birthdaySchema, nullable: true }, birthday: { ...Users.birthdaySchema, nullable: true },
lang: { type: 'string', enum: [null, ...Object.keys(langmap)], 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.name !== undefined) updates.name = ps.name;
if (ps.description !== undefined) profileUpdates.description = ps.description; if (ps.description !== undefined) profileUpdates.description = ps.description;
if (ps.lang !== undefined) profileUpdates.lang = ps.lang; 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.location !== undefined) profileUpdates.location = ps.location;
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;

View file

@ -17,6 +17,11 @@
<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template> <template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
</FormTextarea> </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"> <FormInput v-model="profile.location" manual-save class="_formBlock">
<template #label>{{ i18n.ts.location }}</template> <template #label>{{ i18n.ts.location }}</template>
<template #prefix><i class="fas fa-map-marker-alt"></i></template> <template #prefix><i class="fas fa-map-marker-alt"></i></template>
@ -82,6 +87,7 @@ import { langmap } from '@/scripts/langmap';
const profile = reactive({ const profile = reactive({
name: $i.name, name: $i.name,
description: $i.description, description: $i.description,
pronouns: $i.pronouns,
location: $i.location, location: $i.location,
birthday: $i.birthday, birthday: $i.birthday,
lang: $i.lang, lang: $i.lang,
@ -120,6 +126,7 @@ function save() {
os.apiWithDialog('i/update', { os.apiWithDialog('i/update', {
name: profile.name || null, name: profile.name || null,
description: profile.description || null, description: profile.description || null,
pronouns: profile.pronouns || null,
location: profile.location || null, location: profile.location || null,
birthday: profile.birthday || null, birthday: profile.birthday || null,
lang: profile.lang || null, lang: profile.lang || null,

View file

@ -47,6 +47,10 @@
<p v-else class="empty">{{ $ts.noAccountDescription }}</p> <p v-else class="empty">{{ $ts.noAccountDescription }}</p>
</div> </div>
<div class="fields system"> <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"> <dl v-if="user.location" class="field">
<dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt> <dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
<dd class="value">{{ user.location }}</dd> <dd class="value">{{ user.location }}</dd>