Task #42: Tell members when someone leaves a group on their own

Original task: Post a bilingual system message into the group chat
when a member leaves via POST /api/conversations/:id/leave, with
real-time delivery via Socket.IO and rendering in the chat thread.

Findings:
- The leave handler in artifacts/api-server/src/routes/conversations.ts
  already inserts a `member_left` system message (and `admin_promoted`
  when a successor is auto-promoted) and emits via Socket.IO using
  insertAndEmitSystemMessage. Direct conversations and last-leaver
  cleanup paths are correctly skipped.
- artifacts/teaboy-os/src/pages/chat.tsx renderSystemMessage already
  handles `member_left`, `admin_promoted`, and `adminPromotedAfterLeave`.
- Bilingual locale keys exist in src/locales/en.json and ar.json under
  chat.system.memberLeft / adminPromoted / adminPromotedAfterLeave.

Change made:
- Extended the MessageWithSender.kind enum in lib/api-spec/openapi.yaml
  to include `member_left` and `admin_promoted` (previously only
  user/group_renamed/members_added/member_removed were declared).
- Re-ran `pnpm --filter @workspace/api-spec run codegen` to regenerate
  api-zod and api-client-react schemas so the spec, runtime validation,
  and client types stay in sync.
- Verified `pnpm -w run typecheck` passes across all packages.

No behavioural code changes were necessary; the feature was already
implemented end-to-end and only the API spec enum needed to catch up.

Replit-Task-Id: 40fccb3a-4fc0-4ad7-a6e9-d1ac67ef7769
This commit is contained in:
riyadhafraa
2026-04-21 10:20:22 +00:00
parent b6a0dc6ab9
commit 62b79c7e7d
3 changed files with 23 additions and 2 deletions
@@ -269,6 +269,8 @@ export const MessageWithSenderKind = {
group_renamed: "group_renamed",
members_added: "members_added",
member_removed: "member_removed",
member_left: "member_left",
admin_promoted: "admin_promoted",
} as const;
export type MessageWithSenderMeta = { [key: string]: unknown } | null;
+1 -1
View File
@@ -1708,7 +1708,7 @@ components:
type: string
kind:
type: string
enum: [user, group_renamed, members_added, member_removed]
enum: [user, group_renamed, members_added, member_removed, member_left, admin_promoted]
meta:
type: object
nullable: true
+20 -1
View File
@@ -596,6 +596,8 @@ export const ListConversationsResponseItem = zod.object({
"group_renamed",
"members_added",
"member_removed",
"member_left",
"admin_promoted",
]),
meta: zod.record(zod.string(), zod.unknown()).nullish(),
sender: zod.object({
@@ -666,6 +668,8 @@ export const GetConversationResponse = zod.object({
"group_renamed",
"members_added",
"member_removed",
"member_left",
"admin_promoted",
]),
meta: zod.record(zod.string(), zod.unknown()).nullish(),
sender: zod.object({
@@ -728,6 +732,8 @@ export const UpdateConversationResponse = zod.object({
"group_renamed",
"members_added",
"member_removed",
"member_left",
"admin_promoted",
]),
meta: zod.record(zod.string(), zod.unknown()).nullish(),
sender: zod.object({
@@ -788,6 +794,8 @@ export const AddConversationParticipantsResponse = zod.object({
"group_renamed",
"members_added",
"member_removed",
"member_left",
"admin_promoted",
]),
meta: zod.record(zod.string(), zod.unknown()).nullish(),
sender: zod.object({
@@ -845,6 +853,8 @@ export const RemoveConversationParticipantResponse = zod.object({
"group_renamed",
"members_added",
"member_removed",
"member_left",
"admin_promoted",
]),
meta: zod.record(zod.string(), zod.unknown()).nullish(),
sender: zod.object({
@@ -906,6 +916,8 @@ export const UpdateConversationStateResponse = zod.object({
"group_renamed",
"members_added",
"member_removed",
"member_left",
"admin_promoted",
]),
meta: zod.record(zod.string(), zod.unknown()).nullish(),
sender: zod.object({
@@ -950,7 +962,14 @@ export const ListMessagesResponseItem = zod.object({
conversationId: zod.number(),
senderId: zod.number(),
content: zod.string(),
kind: zod.enum(["user", "group_renamed", "members_added", "member_removed"]),
kind: zod.enum([
"user",
"group_renamed",
"members_added",
"member_removed",
"member_left",
"admin_promoted",
]),
meta: zod.record(zod.string(), zod.unknown()).nullish(),
sender: zod.object({
id: zod.number(),