feat(messenger): group settings UI — rename, member list, add/remove

- messenger-api: useGroupSettings(threadId) — members query + rename/add/
  remove commands + isAdmin (from the member roles); live + mock
- messenger: a settings (gear) button on group thread headers opens a
  GroupSettingsModal — editable name (admin), member list with role pills,
  admin-gated remove, and add-from-directory search
- controls are admin-gated in the UI; IIOS/OPA re-enforces server-side

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 17:43:52 +05:30
parent 0b433790a5
commit 4ad0decad0
2 changed files with 162 additions and 4 deletions
+61
View File
@@ -87,12 +87,26 @@ export interface ThreadData {
refetch: () => void;
}
export interface UiMember { userId: string; displayName: string; role: string }
export interface GroupSettingsData {
loading: boolean; error: string | null;
members: UiMember[];
isAdmin: boolean;
rename: (subject: string) => Promise<void>;
addMember: (userId: string) => Promise<void>;
removeMember: (userId: string) => Promise<void>;
refetch: () => void;
}
export function useMessengerData(): MessengerData {
return SHELL ? useLiveMessenger() : useMockMessenger();
}
export function useThread(threadId: string): ThreadData {
return SHELL ? useLiveThread(threadId) : useMockThread(threadId);
}
export function useGroupSettings(threadId: string): GroupSettingsData {
return SHELL ? useLiveGroupSettings(threadId) : useMockGroupSettings(threadId);
}
/* ======================================================================== */
/* Live implementation (be-crm data door + IIOS socket) */
@@ -259,6 +273,29 @@ function useLiveThread(threadId: string): ThreadData {
};
}
function useLiveGroupSettings(threadId: string): GroupSettingsData {
const { sdk } = useAppShell();
const { user } = useAuth();
const q = useQuery<UiMember[]>("crm.messenger.members", { threadId });
const members = useMemo(() => q.data ?? [], [q.data]);
const isAdmin = useMemo(() => members.some((m) => m.userId === user?.id && m.role === "ADMIN"), [members, user?.id]);
const rename = useCallback(async (subject: string) => {
await sdk.command("crm.messenger.group.rename", { threadId, subject });
q.refetch();
}, [sdk, threadId, q]);
const addMember = useCallback(async (userId: string) => {
await sdk.command("crm.messenger.participant.add", { threadId, userId });
q.refetch();
}, [sdk, threadId, q]);
const removeMember = useCallback(async (userId: string) => {
await sdk.command("crm.messenger.participant.remove", { threadId, userId });
q.refetch();
}, [sdk, threadId, q]);
return { loading: q.loading, error: q.error?.message ?? null, members, isAdmin, rename, addMember, removeMember, refetch: q.refetch };
}
function shape(
c: ConversationDTO,
nameOf: (id: string) => string,
@@ -372,3 +409,27 @@ function useMockThread(threadId: string): ThreadData {
typingUserIds: [], seenIds: new Set(), refetch: notifyMock,
};
}
function useMockGroupSettings(threadId: string): GroupSettingsData {
useMockSubscription();
const nameById = useMemo(() => Object.fromEntries(MOCK_PEOPLE.map((p) => [p.id, p.name])), []);
const t = MOCK_STORE.get(threadId);
const members: UiMember[] = (t?.participants ?? []).map((id) => ({
userId: id,
displayName: id === "me" ? "You" : (nameById[id] ?? `User ${shortId(id)}`),
role: id === "me" ? "ADMIN" : "MEMBER",
}));
const rename = useCallback(async (subject: string) => {
const th = MOCK_STORE.get(threadId);
if (th) { th.subject = subject; notifyMock(); }
}, [threadId]);
const addMember = useCallback(async (userId: string) => {
const th = MOCK_STORE.get(threadId);
if (th && !th.participants.includes(userId)) { th.participants = [...th.participants, userId]; notifyMock(); }
}, [threadId]);
const removeMember = useCallback(async (userId: string) => {
const th = MOCK_STORE.get(threadId);
if (th) { th.participants = th.participants.filter((p) => p !== userId); notifyMock(); }
}, [threadId]);
return { loading: false, error: null, members, isAdmin: true, rename, addMember, removeMember, refetch: notifyMock };
}