From 20ca6e03341fbacb3954935c1693b91a2632525d Mon Sep 17 00:00:00 2001 From: Antoine Piron Date: Thu, 11 Jun 2026 21:54:49 +0200 Subject: [PATCH] feat(compose): group channels by customer with tap-to-select-all Channels in the compose picker are now grouped by customer (as returned by the Postiz API). Tapping a customer header selects or deselects all its channels at once. Individual channel chips still toggle as before. Workspace header is hidden when only one workspace is configured. Co-Authored-By: Claude Sonnet 4.6 --- .../postiz-mobile/app/(tabs)/compose.tsx | 142 +++++++++++++----- .../postiz-mobile/context/PostizContext.tsx | 1 + 2 files changed, 106 insertions(+), 37 deletions(-) diff --git a/artifacts/postiz-mobile/app/(tabs)/compose.tsx b/artifacts/postiz-mobile/app/(tabs)/compose.tsx index 0cbc47e..30edb5e 100644 --- a/artifacts/postiz-mobile/app/(tabs)/compose.tsx +++ b/artifacts/postiz-mobile/app/(tabs)/compose.tsx @@ -140,8 +140,20 @@ export default function ComposeScreen() { staleTime: 60000, }); - // Group: workspace → flat list of integrations - const grouped = useMemo(() => { + type CustomerGroup = { + customerId: string; + customerName: string; + channels: IntegrationWithWorkspace[]; + }; + + type WorkspaceGroup = { + workspace: PostizWorkspace; + customers: CustomerGroup[]; + allChannels: IntegrationWithWorkspace[]; + }; + + // Group: workspace → customers → channels + const grouped = useMemo((): WorkspaceGroup[] => { if (!allIntegrations) return []; const byWorkspace = new Map(); for (const intg of allIntegrations) { @@ -150,12 +162,21 @@ export default function ComposeScreen() { } return workspaces .filter((ws) => byWorkspace.has(ws.id)) - .map((ws) => ({ workspace: ws, channels: byWorkspace.get(ws.id)! })); + .map((ws) => { + const allChannels = byWorkspace.get(ws.id)!; + const byCustomer = new Map(); + for (const ch of allChannels) { + const cId = ch.customer?.id ?? "__default__"; + const cName = ch.customer?.name ?? ws.name; + if (!byCustomer.has(cId)) byCustomer.set(cId, { customerId: cId, customerName: cName, channels: [] }); + byCustomer.get(cId)!.channels.push(ch); + } + return { workspace: ws, customers: Array.from(byCustomer.values()), allChannels }; + }); }, [allIntegrations, workspaces]); const toggleWorkspace = (wsId: string) => { - const wsChannels = grouped.find((g) => g.workspace.id === wsId)?.channels ?? []; - const allIds = wsChannels.map((c) => c.id); + const allIds = (grouped.find((g) => g.workspace.id === wsId)?.allChannels ?? []).map((c) => c.id); const allSelected = allIds.every((id) => selectedChannels.includes(id)); if (allSelected) { setSelectedChannels((prev) => prev.filter((id) => !allIds.includes(id))); @@ -164,6 +185,15 @@ export default function ComposeScreen() { } }; + const toggleCustomer = (customerIds: string[]) => { + const allSelected = customerIds.every((id) => selectedChannels.includes(id)); + if (allSelected) { + setSelectedChannels((prev) => prev.filter((id) => !customerIds.includes(id))); + } else { + setSelectedChannels((prev) => [...new Set([...prev, ...customerIds])]); + } + }; + const effectiveCharLimit = useMemo(() => { if (selectedChannels.length === 0 || !allIntegrations) return 3000; const selected = allIntegrations.filter((i) => selectedChannels.includes(i.id)); @@ -535,11 +565,11 @@ export default function ComposeScreen() { ) : ( - {grouped.map(({ workspace, channels }, wsIdx) => { - const allIds = channels.map((c) => c.id); - const selectedCount = allIds.filter((id) => selectedChannels.includes(id)).length; - const allSelected = selectedCount === allIds.length; - const someSelected = selectedCount > 0 && !allSelected; + {grouped.map(({ workspace, customers, allChannels }, wsIdx) => { + const wsAllIds = allChannels.map((c) => c.id); + const wsSelectedCount = wsAllIds.filter((id) => selectedChannels.includes(id)).length; + const wsAllSelected = wsSelectedCount === wsAllIds.length; + const wsSomeSelected = wsSelectedCount > 0 && !wsAllSelected; return ( {/* Workspace header — tap to select/deselect all */} - toggleWorkspace(workspace.id)} - style={[styles.workspaceHeader, { borderBottomColor: colors.border }]} - > - - - {workspace.name} - - - - - {/* Flat channel chips */} - - {channels.map((intg) => ( - toggleChannel(intg.id)} + {workspaces.length > 1 && ( + toggleWorkspace(workspace.id)} + style={[styles.workspaceHeader, { borderBottomColor: colors.border }]} + > + + + {workspace.name} + + - ))} - + + )} + + {/* Customer sub-sections */} + {customers.map((cust, cIdx) => { + const custIds = cust.channels.map((c) => c.id); + const custSelectedCount = custIds.filter((id) => selectedChannels.includes(id)).length; + const custAllSelected = custSelectedCount === custIds.length; + const custSomeSelected = custSelectedCount > 0 && !custAllSelected; + return ( + 0 ? [styles.customerSection, { borderTopColor: colors.border }] : undefined} + > + {/* Customer header — tap to select/deselect all channels for this customer */} + toggleCustomer(custIds)} + style={styles.customerHeader} + > + + {cust.customerName} + + + + + {/* Channel chips for this customer */} + + {cust.channels.map((intg) => ( + toggleChannel(intg.id)} + /> + ))} + + + ); + })} ); })} @@ -761,7 +823,13 @@ const styles = StyleSheet.create({ borderBottomWidth: StyleSheet.hairlineWidth, }, workspaceName: { fontSize: 11, fontFamily: "Inter_600SemiBold", letterSpacing: 0.5 }, - chipRow: { flexDirection: "row", flexWrap: "wrap", gap: 6, padding: 10 }, + customerSection: { borderTopWidth: StyleSheet.hairlineWidth }, + customerHeader: { + flexDirection: "row", alignItems: "center", justifyContent: "space-between", + paddingHorizontal: 12, paddingVertical: 8, + }, + customerName: { fontSize: 13, fontFamily: "Inter_600SemiBold" }, + chipRow: { flexDirection: "row", flexWrap: "wrap", gap: 6, paddingHorizontal: 10, paddingBottom: 10 }, scheduleRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 16, paddingVertical: 12, borderRadius: 12, borderWidth: 1, diff --git a/artifacts/postiz-mobile/context/PostizContext.tsx b/artifacts/postiz-mobile/context/PostizContext.tsx index a0959d7..f86cd46 100644 --- a/artifacts/postiz-mobile/context/PostizContext.tsx +++ b/artifacts/postiz-mobile/context/PostizContext.tsx @@ -28,6 +28,7 @@ export interface PostizIntegration { picture?: string; identifier?: string; internalType?: string; + customer?: { id: string; name: string }; } export interface PostizMediaItem {