public profile editing

This commit is contained in:
Moon.eth
2026-07-29 19:51:59 +09:00
parent 8f0d47b850
commit 17f385c1c3
9 changed files with 795 additions and 14 deletions
+51 -1
View File
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { ApiClient } from './client'
import { postStatus, updateProfileFields, votePoll } from './endpoints'
import { postStatus, updateProfileFields, updatePublicProfile, votePoll } from './endpoints'
afterEach(() => {
vi.unstubAllGlobals()
@@ -39,6 +39,56 @@ describe('updateProfileFields', () => {
})
})
describe('updatePublicProfile', () => {
it('sends public text, images, fields and Pleroma identity data as multipart form data', async () => {
const avatar = new File(['avatar'], 'avatar.png', { type: 'image/png' })
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
const form = init?.body as FormData
expect(init?.method).toBe('PATCH')
expect(form.get('display_name')).toBe('Alice Example')
expect(form.get('note')).toBe('My public bio')
expect(form.get('avatar')).toBe(avatar)
expect(form.get('header')).toBe('')
expect(form.get('actor_type')).toBe('Group')
expect(form.get('birthday')).toBe('2000-01-02')
expect(form.get('show_birthday')).toBe('true')
expect(form.get('fields_attributes[0][name]')).toBe('Website')
expect(form.get('fields_attributes[0][value]')).toBe('https://example.test')
return new Response(JSON.stringify({ id: 'account-1', fields: [] }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
})
vi.stubGlobal('fetch', fetchMock)
await updatePublicProfile(new ApiClient('example.test', 'token'), {
displayName: 'Alice Example',
note: 'My public bio',
avatar,
header: '',
actorType: 'Group',
birthday: '2000-01-02',
showBirthday: true,
fields: [{ name: 'Website', value: 'https://example.test' }],
})
})
it('sends an empty sentinel row when all public fields are cleared', async () => {
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
const form = init?.body as FormData
expect(form.get('fields_attributes[0][name]')).toBe('')
expect(form.get('fields_attributes[0][value]')).toBe('')
return new Response(JSON.stringify({ id: 'account-1', fields: [] }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
})
vi.stubGlobal('fetch', fetchMock)
await updatePublicProfile(new ApiClient('example.test', 'token'), { fields: [] })
})
})
describe('poll endpoints', () => {
it('submits selected poll option indexes as JSON', async () => {
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
+58 -10
View File
@@ -86,6 +86,63 @@ export function verifyCredentials(api: ApiClient): Promise<CredentialAccount> {
return api.get<CredentialAccount>('/api/v1/accounts/verify_credentials')
}
export interface PublicProfileUpdate {
displayName?: string
note?: string
fields?: Array<{ name: string; value: string }>
avatar?: File | '' | undefined
header?: File | '' | undefined
background?: File | '' | undefined
avatarDescription?: string
headerDescription?: string
bot?: boolean
actorType?: 'Person' | 'Service' | 'Group'
birthday?: string
showBirthday?: boolean
}
/** Update only public-facing account profile data through the shared API. */
export async function updatePublicProfile(
api: ApiClient,
update: PublicProfileUpdate,
): Promise<CredentialAccount> {
const form = new FormData()
if (update.displayName !== undefined) form.set('display_name', update.displayName)
if (update.note !== undefined) form.set('note', update.note)
if (update.avatar !== undefined) form.set('avatar', update.avatar)
if (update.header !== undefined) form.set('header', update.header)
if (update.background !== undefined) form.set('pleroma_background_image', update.background)
if (update.avatarDescription !== undefined) {
form.set('avatar_description', update.avatarDescription)
}
if (update.headerDescription !== undefined) {
form.set('header_description', update.headerDescription)
}
if (update.bot !== undefined) form.set('bot', String(update.bot))
if (update.actorType !== undefined) form.set('actor_type', update.actorType)
if (update.birthday !== undefined) form.set('birthday', update.birthday)
if (update.showBirthday !== undefined) form.set('show_birthday', String(update.showBirthday))
// An omitted collection means "leave fields unchanged". To explicitly clear
// every field, submit one empty row; Pleroma and Mastodon both discard it
// while still recognizing that fields_attributes was present.
const submittedFields =
update.fields === undefined
? undefined
: update.fields.length > 0
? update.fields
: [{ name: '', value: '' }]
submittedFields?.forEach((field, index) => {
form.append(`fields_attributes[${index}][name]`, field.name)
form.append(`fields_attributes[${index}][value]`, field.value)
})
const { data } = await api.raw<CredentialAccount>('/api/v1/accounts/update_credentials', {
method: 'PATCH',
form,
})
return data
}
/**
* Replace the signed-in account's profile fields while leaving every other
* credential untouched. Mastodon and Pleroma both accept this indexed
@@ -95,16 +152,7 @@ export async function updateProfileFields(
api: ApiClient,
fields: Array<{ name: string; value: string }>,
): Promise<CredentialAccount> {
const form = new FormData()
fields.forEach((field, index) => {
form.append(`fields_attributes[${index}][name]`, field.name)
form.append(`fields_attributes[${index}][value]`, field.value)
})
const { data } = await api.raw<CredentialAccount>('/api/v1/accounts/update_credentials', {
method: 'PATCH',
form,
})
return data
return updatePublicProfile(api, { fields })
}
export function fetchAccount(api: ApiClient, id: string): Promise<Account> {
+10
View File
@@ -47,8 +47,10 @@ export interface Account {
uri?: string
avatar: string
avatar_static: string
avatar_description?: string | null
header: string
header_static: string
header_description?: string | null
locked: boolean
bot?: boolean
group?: boolean
@@ -84,6 +86,9 @@ export interface Account {
relationship?: Relationship
/** Some deployments expose the user's own profile CSS here. */
background_color?: string | null
birthday?: string | null
avatar_description?: string | null
header_description?: string | null
}
}
@@ -95,6 +100,11 @@ export interface CredentialAccount extends Account {
sensitive: boolean
language: string | null
follow_requests_count?: number
pleroma?: {
actor_type?: 'Person' | 'Service' | 'Group'
show_birthday?: boolean
[key: string]: unknown
}
}
}