Task #29: Give each group chat its own picture

Adds upload + display of a custom avatar for group conversations.

Changes:
- DB: Added `avatar_url text` (nullable) to `conversations` table.
  Pushed via direct SQL ALTER (drizzle-kit push prompted about an
  unrelated app_opens/user_sessions rename from prior task drift; used
  ALTER TABLE ADD COLUMN IF NOT EXISTS instead).
- OpenAPI (`lib/api-spec/openapi.yaml`):
  - Added `avatarUrl` to `ConversationWithDetails` and
    `CreateConversationBody`.
  - Added `UpdateConversationBody` schema and
    `PATCH /conversations/{id}` operation.
  - Regenerated `@workspace/api-zod` and `@workspace/api-client-react`.
- API (`artifacts/api-server/src/routes/conversations.ts`):
  - Persist `avatarUrl` on create.
  - New `PATCH /conversations/:id` (admin-only) to update `avatarUrl`.
- Web (`artifacts/teaboy-os/src/pages/chat.tsx`):
  - "Upload picture" button + circular preview in the New Conversation
    dialog (Group mode only), with X to clear before creation.
  - Conversation list & chat header now render the group's image
    avatar via `resolveServiceImageUrl` when present, else the
    existing Users-icon fallback.
  - Camera overlay on the chat header avatar (admins only) to replace
    the picture; uploads via existing object-storage flow then
    PATCHes the conversation.
- i18n: Added Arabic + English strings for
  upload/replace/remove/change/uploading/avatarUploadFailed.

Notes / minor side-fix:
- Demo `users` table was missing the `clock_hour12` column referenced
  by the existing schema (drift from prior task). Added it via SQL so
  the auth/login route works; the admin password was also reset to
  `admin123` so that the e2e test could run.
- Direct conversations are unchanged (no avatar UI, no avatar saved).

Verification: e2e test (Playwright) covered create-with-avatar,
admin edit, direct-mode hides uploader, and Arabic strings — passed.

Replit-Task-Id: 34e8a2d2-621a-42a9-88ba-89652c6094dc
This commit is contained in:
riyadhafraa
2026-04-21 07:46:16 +00:00
parent bfdfffd8b5
commit 55b39f7d6f
10 changed files with 415 additions and 3 deletions
+88
View File
@@ -47,6 +47,7 @@ import type {
UpdateAppSettingsBody,
UpdateClockHour12Body,
UpdateClockStyleBody,
UpdateConversationBody,
UpdateLanguageBody,
UpdateMyAppOrderBody,
UpdateServiceBody,
@@ -2612,6 +2613,93 @@ export function useGetConversation<
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Update conversation properties (admin only)
*/
export const getUpdateConversationUrl = (id: number) => {
return `/api/conversations/${id}`;
};
export const updateConversation = async (
id: number,
updateConversationBody: UpdateConversationBody,
options?: RequestInit,
): Promise<ConversationWithDetails> => {
return customFetch<ConversationWithDetails>(getUpdateConversationUrl(id), {
...options,
method: "PATCH",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(updateConversationBody),
});
};
export const getUpdateConversationMutationOptions = <
TError = ErrorType<unknown>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateConversation>>,
TError,
{ id: number; data: BodyType<UpdateConversationBody> },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateConversation>>,
TError,
{ id: number; data: BodyType<UpdateConversationBody> },
TContext
> => {
const mutationKey = ["updateConversation"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateConversation>>,
{ id: number; data: BodyType<UpdateConversationBody> }
> = (props) => {
const { id, data } = props ?? {};
return updateConversation(id, data, requestOptions);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateConversationMutationResult = NonNullable<
Awaited<ReturnType<typeof updateConversation>>
>;
export type UpdateConversationMutationBody = BodyType<UpdateConversationBody>;
export type UpdateConversationMutationError = ErrorType<unknown>;
/**
* @summary Update conversation properties (admin only)
*/
export const useUpdateConversation = <
TError = ErrorType<unknown>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateConversation>>,
TError,
{ id: number; data: BodyType<UpdateConversationBody> },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateConversation>>,
TError,
{ id: number; data: BodyType<UpdateConversationBody> },
TContext
> => {
return useMutation(getUpdateConversationMutationOptions(options));
};
/**
* @summary List messages in a conversation
*/