feat: avatars for humans (#273)

* implement avatar backend functionality

* add avatar endpoints

* typo

* implement client side avatar upload and handling

* fixes

* Update go/internal/handler/handler.go

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update js/desktop/src/lib/avatar-image.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* bug in order

* remove unused component

* fix invalid migration

* fix syntax errors

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit was merged in pull request #273.
This commit is contained in:
Arjun Patel
2026-06-11 15:30:21 -07:00
committed by GitHub
co-authored by coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
parent ca3bbf204e
commit 4facc6b371
25 changed files with 739 additions and 256 deletions
+34 -8
View File
@@ -42,16 +42,12 @@ class ApiClient {
this.config = config;
}
private async fetch(
private async send(
method: string,
path: string,
body?: unknown,
init: { headers?: Record<string, string>; body?: BodyInit } = {},
): Promise<Response> {
const headers: Record<string, string> = {};
if (body) {
headers['Content-Type'] = 'application/json';
}
const headers: Record<string, string> = { ...init.headers };
const token = this.config.getToken();
if (token) {
@@ -61,7 +57,7 @@ class ApiClient {
const response = await fetch(`${this.config.baseUrl}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
body: init.body,
});
if (response.status === 401) {
@@ -77,6 +73,17 @@ class ApiClient {
return response;
}
private async fetch(
method: string,
path: string,
body?: unknown,
): Promise<Response> {
return this.send(method, path, {
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body: body ? JSON.stringify(body) : undefined,
});
}
private async request<T>(
schema: z.ZodType<T>,
method: string,
@@ -137,6 +144,25 @@ class ApiClient {
await this.requestVoid('PATCH', '/humans/me/settings', data);
}
// --- Avatar ---
async updateAvatar(blob: Blob): Promise<void> {
await this.send('PUT', '/humans/me/avatar', {
headers: { 'Content-Type': blob.type || 'image/jpeg' },
body: blob,
});
}
async deleteAvatar(): Promise<void> {
await this.requestVoid('DELETE', '/humans/me/avatar');
}
async getAvatarDownloadUrl(objectId: string): Promise<string> {
const response = await this.fetch('GET', `/humans/avatar/${objectId}`);
const data = await response.json();
return data.url;
}
// --- Depot ---
async prepareUpload(data: PrepareUploadRequest) {
+1
View File
@@ -6,6 +6,7 @@ export const HumanSchema = z.object({
email: z.string().email(),
email_prefix: z.string(),
email_notifications_enabled: z.boolean(),
avatar_object_id: z.string().nullable().optional(),
});
export type Human = z.infer<typeof HumanSchema>;