feat(compose): tap-to-select-all workspace + flat channel chips
Release APK / build (push) Has been cancelled

- Workspace header is now a TouchableOpacity: tap selects all its
  channels, re-tap deselects all (partial state shows minus-square icon)
- Removed sub-grouping by network type — channels are displayed as a
  flat chip row directly under each workspace card
- Removed unused networkLabel helper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 18:46:35 +02:00
parent 59b688dafb
commit 9abd05d05a
+57 -76
View File
@@ -56,22 +56,6 @@ type MediaItem =
| { type: "local"; uri: string } | { type: "local"; uri: string }
| { type: "uploaded"; id: string; path: string; workspaceId: string }; | { type: "uploaded"; id: string; path: string; workspaceId: string };
// Maps a type string to a display label, used for grouping within a workspace
function networkLabel(intg: PostizIntegration): string {
const t = (intg.type ?? intg.internalType ?? "").toLowerCase();
if (t.includes("twitter") || t.includes("x-") || t === "x") return "X / Twitter";
if (t.includes("instagram")) return "Instagram";
if (t.includes("linkedin")) return "LinkedIn";
if (t.includes("facebook")) return "Facebook";
if (t.includes("tiktok")) return "TikTok";
if (t.includes("youtube")) return "YouTube";
if (t.includes("pinterest")) return "Pinterest";
if (t.includes("mastodon")) return "Mastodon";
if (t.includes("bluesky") || t.includes("bsky")) return "Bluesky";
if (t.includes("threads")) return "Threads";
if (t.includes("reddit")) return "Reddit";
return "Other";
}
function resolveMediaUrl(path: string, baseUrl: string): string { function resolveMediaUrl(path: string, baseUrl: string): string {
if (path.startsWith("http://") || path.startsWith("https://")) return path; if (path.startsWith("http://") || path.startsWith("https://")) return path;
@@ -156,7 +140,7 @@ export default function ComposeScreen() {
staleTime: 60000, staleTime: 60000,
}); });
// Group: workspace → network label → integrations // Group: workspace → flat list of integrations
const grouped = useMemo(() => { const grouped = useMemo(() => {
if (!allIntegrations) return []; if (!allIntegrations) return [];
const byWorkspace = new Map<string, IntegrationWithWorkspace[]>(); const byWorkspace = new Map<string, IntegrationWithWorkspace[]>();
@@ -166,21 +150,20 @@ export default function ComposeScreen() {
} }
return workspaces return workspaces
.filter((ws) => byWorkspace.has(ws.id)) .filter((ws) => byWorkspace.has(ws.id))
.map((ws) => { .map((ws) => ({ workspace: ws, channels: byWorkspace.get(ws.id)! }));
const intgs = byWorkspace.get(ws.id)!;
const byNetwork = new Map<string, IntegrationWithWorkspace[]>();
for (const intg of intgs) {
const key = networkLabel(intg);
if (!byNetwork.has(key)) byNetwork.set(key, []);
byNetwork.get(key)!.push(intg);
}
return {
workspace: ws,
networks: Array.from(byNetwork.entries()).map(([label, channels]) => ({ label, channels })),
};
});
}, [allIntegrations, workspaces]); }, [allIntegrations, workspaces]);
const toggleWorkspace = (wsId: string) => {
const wsChannels = grouped.find((g) => g.workspace.id === wsId)?.channels ?? [];
const allIds = wsChannels.map((c) => c.id);
const allSelected = allIds.every((id) => selectedChannels.includes(id));
if (allSelected) {
setSelectedChannels((prev) => prev.filter((id) => !allIds.includes(id)));
} else {
setSelectedChannels((prev) => [...new Set([...prev, ...allIds])]);
}
};
const effectiveCharLimit = useMemo(() => { const effectiveCharLimit = useMemo(() => {
if (selectedChannels.length === 0 || !allIntegrations) return 3000; if (selectedChannels.length === 0 || !allIntegrations) return 3000;
const selected = allIntegrations.filter((i) => selectedChannels.includes(i.id)); const selected = allIntegrations.filter((i) => selectedChannels.includes(i.id));
@@ -552,50 +535,51 @@ export default function ComposeScreen() {
</Text> </Text>
) : ( ) : (
<View style={styles.channelGroups}> <View style={styles.channelGroups}>
{grouped.map(({ workspace, networks }, wsIdx) => ( {grouped.map(({ workspace, channels }, wsIdx) => {
<View const allIds = channels.map((c) => c.id);
key={workspace.id} const selectedCount = allIds.filter((id) => selectedChannels.includes(id)).length;
style={[ const allSelected = selectedCount === allIds.length;
styles.workspaceSection, const someSelected = selectedCount > 0 && !allSelected;
{ return (
backgroundColor: colors.card, <View
borderColor: colors.border, key={workspace.id}
}, style={[
wsIdx > 0 && { marginTop: 8 }, styles.workspaceSection,
]} { backgroundColor: colors.card, borderColor: colors.border },
> wsIdx > 0 && { marginTop: 8 },
{/* Workspace header */} ]}
<View style={[styles.workspaceHeader, { borderBottomColor: colors.border }]}> >
<Feather name="briefcase" size={12} color={colors.primary} /> {/* Workspace header — tap to select/deselect all */}
<Text style={[styles.workspaceName, { color: colors.primary }]}> <TouchableOpacity
{workspace.name} activeOpacity={0.7}
</Text> onPress={() => toggleWorkspace(workspace.id)}
</View> style={[styles.workspaceHeader, { borderBottomColor: colors.border }]}
>
<Feather name="briefcase" size={12} color={colors.primary} />
<Text style={[styles.workspaceName, { color: colors.primary, flex: 1 }]}>
{workspace.name}
</Text>
<Feather
name={allSelected ? "check-square" : someSelected ? "minus-square" : "square"}
size={14}
color={allSelected || someSelected ? colors.primary : colors.mutedForeground}
/>
</TouchableOpacity>
{/* Network groups */} {/* Flat channel chips */}
<View style={styles.networkGroups}> <View style={styles.chipRow}>
{networks.map(({ label, channels }) => ( {channels.map((intg) => (
<View key={label} style={styles.networkGroup}> <ChannelChip
{networks.length > 1 && ( key={intg.id}
<Text style={[styles.networkLabel, { color: colors.mutedForeground }]}> integration={intg}
{label} selected={selectedChannels.includes(intg.id)}
</Text> onToggle={() => toggleChannel(intg.id)}
)} />
<View style={styles.chipRow}> ))}
{channels.map((intg) => ( </View>
<ChannelChip
key={intg.id}
integration={intg}
selected={selectedChannels.includes(intg.id)}
onToggle={() => toggleChannel(intg.id)}
/>
))}
</View>
</View>
))}
</View> </View>
</View> );
))} })}
</View> </View>
)} )}
@@ -777,10 +761,7 @@ const styles = StyleSheet.create({
borderBottomWidth: StyleSheet.hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
}, },
workspaceName: { fontSize: 11, fontFamily: "Inter_600SemiBold", letterSpacing: 0.5 }, workspaceName: { fontSize: 11, fontFamily: "Inter_600SemiBold", letterSpacing: 0.5 },
networkGroups: { padding: 10, gap: 10 }, chipRow: { flexDirection: "row", flexWrap: "wrap", gap: 6, padding: 10 },
networkGroup: { gap: 4 },
networkLabel: { fontSize: 10, fontFamily: "Inter_500Medium", letterSpacing: 0.4, marginLeft: 2 },
chipRow: { flexDirection: "row", flexWrap: "wrap", gap: 6 },
scheduleRow: { scheduleRow: {
flexDirection: "row", alignItems: "center", justifyContent: "space-between", flexDirection: "row", alignItems: "center", justifyContent: "space-between",
paddingHorizontal: 16, paddingVertical: 12, borderRadius: 12, borderWidth: 1, paddingHorizontal: 16, paddingVertical: 12, borderRadius: 12, borderWidth: 1,