Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0cf5800463 | |||
| 20ca6e0334 |
@@ -68,12 +68,11 @@ export default function ComposeScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { workspaces, clients, isConfigured } = usePostiz();
|
||||
const queryClient = useQueryClient();
|
||||
const { prefillContent, prefillIntegrationIds, prefillImagePath, prefillImageId } =
|
||||
const { prefillContent, prefillIntegrationIds, prefillImages } =
|
||||
useLocalSearchParams<{
|
||||
prefillContent?: string;
|
||||
prefillIntegrationIds?: string;
|
||||
prefillImagePath?: string;
|
||||
prefillImageId?: string;
|
||||
prefillImages?: string;
|
||||
}>();
|
||||
|
||||
const [content, setContent] = useState("");
|
||||
@@ -95,12 +94,23 @@ export default function ComposeScreen() {
|
||||
if (prefillIntegrationIds) {
|
||||
setSelectedChannels(String(prefillIntegrationIds).split(",").filter(Boolean));
|
||||
}
|
||||
if (prefillImagePath && prefillImageId) {
|
||||
// Prefilled image has unknown workspace; associate with first workspace
|
||||
const wsId = workspaces[0]?.id ?? "";
|
||||
setMediaItems([{ type: "uploaded", id: String(prefillImageId), path: String(prefillImagePath), workspaceId: wsId }]);
|
||||
if (prefillImages && workspaces.length > 0) {
|
||||
try {
|
||||
const images: Array<{ id: string; path: string }> = JSON.parse(String(prefillImages));
|
||||
const wsId = workspaces[0]?.id ?? "";
|
||||
setMediaItems(
|
||||
images
|
||||
.filter((img) => img?.id && img?.path)
|
||||
.map((img): MediaItem => ({
|
||||
type: "uploaded",
|
||||
id: img.id,
|
||||
path: img.path,
|
||||
workspaceId: wsId,
|
||||
}))
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
}, [prefillContent, prefillIntegrationIds, prefillImagePath, prefillImageId, workspaces]);
|
||||
}, [prefillContent, prefillIntegrationIds, prefillImages, workspaces]);
|
||||
|
||||
useEffect(() => {
|
||||
if (prefillContent) return;
|
||||
@@ -140,8 +150,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<string, IntegrationWithWorkspace[]>();
|
||||
for (const intg of allIntegrations) {
|
||||
@@ -150,12 +172,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<string, CustomerGroup>();
|
||||
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 +195,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 +575,11 @@ export default function ComposeScreen() {
|
||||
</Text>
|
||||
) : (
|
||||
<View style={styles.channelGroups}>
|
||||
{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 (
|
||||
<View
|
||||
key={workspace.id}
|
||||
@@ -550,33 +590,65 @@ export default function ComposeScreen() {
|
||||
]}
|
||||
>
|
||||
{/* Workspace header — tap to select/deselect all */}
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={() => toggleWorkspace(workspace.id)}
|
||||
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>
|
||||
|
||||
{/* Flat channel chips */}
|
||||
<View style={styles.chipRow}>
|
||||
{channels.map((intg) => (
|
||||
<ChannelChip
|
||||
key={intg.id}
|
||||
integration={intg}
|
||||
selected={selectedChannels.includes(intg.id)}
|
||||
onToggle={() => toggleChannel(intg.id)}
|
||||
{workspaces.length > 1 && (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={() => toggleWorkspace(workspace.id)}
|
||||
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={wsAllSelected ? "check-square" : wsSomeSelected ? "minus-square" : "square"}
|
||||
size={14}
|
||||
color={wsAllSelected || wsSomeSelected ? colors.primary : colors.mutedForeground}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* 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 (
|
||||
<View
|
||||
key={cust.customerId}
|
||||
style={cIdx > 0 ? [styles.customerSection, { borderTopColor: colors.border }] : undefined}
|
||||
>
|
||||
{/* Customer header — tap to select/deselect all channels for this customer */}
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={() => toggleCustomer(custIds)}
|
||||
style={styles.customerHeader}
|
||||
>
|
||||
<Text style={[styles.customerName, { color: colors.foreground }]}>
|
||||
{cust.customerName}
|
||||
</Text>
|
||||
<Feather
|
||||
name={custAllSelected ? "check-square" : custSomeSelected ? "minus-square" : "square"}
|
||||
size={14}
|
||||
color={custAllSelected || custSomeSelected ? colors.primary : colors.mutedForeground}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Channel chips for this customer */}
|
||||
<View style={styles.chipRow}>
|
||||
{cust.channels.map((intg) => (
|
||||
<ChannelChip
|
||||
key={intg.id}
|
||||
integration={intg}
|
||||
selected={selectedChannels.includes(intg.id)}
|
||||
onToggle={() => toggleChannel(intg.id)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
@@ -761,7 +833,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,
|
||||
|
||||
@@ -152,13 +152,14 @@ export default function PostsScreen() {
|
||||
|
||||
const handlePrefillCompose = (post: PostizPost) => {
|
||||
const integrations = post.integrations ?? (post.integration ? [post.integration] : []);
|
||||
router.push({
|
||||
pathname: "/(tabs)/compose",
|
||||
params: {
|
||||
prefillContent: stripHtml(post.content),
|
||||
prefillIntegrationIds: integrations.map((i) => i.id).join(","),
|
||||
},
|
||||
});
|
||||
const params: Record<string, string> = {
|
||||
prefillContent: stripHtml(post.content),
|
||||
prefillIntegrationIds: integrations.map((i) => i.id).join(","),
|
||||
};
|
||||
if (post.image?.length) {
|
||||
params.prefillImages = JSON.stringify(post.image);
|
||||
}
|
||||
router.push({ pathname: "/(tabs)/compose", params });
|
||||
};
|
||||
|
||||
const startReschedule = (post: PostizPost) => {
|
||||
|
||||
@@ -66,12 +66,20 @@ export function MediaLibraryModal({ visible, workspaces, defaultWorkspaceId, max
|
||||
if (!activeWorkspace) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const url = `${activeWorkspace.baseUrl}/media`;
|
||||
try {
|
||||
// eslint-disable-next-line no-undef
|
||||
const res = await globalThis.fetch(`${activeWorkspace.baseUrl}/media`, {
|
||||
const res = await globalThis.fetch(url, {
|
||||
headers: { Authorization: activeWorkspace.apiKey },
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
if (!res.ok) {
|
||||
if (res.status === 404) {
|
||||
throw new Error(
|
||||
`Media listing endpoint not found (404).\nURL tried: ${url}\n\nThis feature requires Postiz to expose GET /media in its public API. Your version may not support it yet.`
|
||||
);
|
||||
}
|
||||
throw new Error(`HTTP ${res.status} — ${url}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
const list: RawMediaItem[] = Array.isArray(data)
|
||||
? data
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface PostizIntegration {
|
||||
picture?: string;
|
||||
identifier?: string;
|
||||
internalType?: string;
|
||||
customer?: { id: string; name: string };
|
||||
}
|
||||
|
||||
export interface PostizMediaItem {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export function stripHtml(html: string): string {
|
||||
export function stripHtml(html: string | null | undefined): string {
|
||||
if (!html) return "";
|
||||
// Decode entities first so encoded tags like <p> are also stripped
|
||||
let s = html
|
||||
.replace(/&/g, "&")
|
||||
|
||||
Reference in New Issue
Block a user