2 Commits

Author SHA1 Message Date
billisdead fb64b671d0 fix(mobile): replace 404 media library error with session-required state and device gallery fallback
Release APK / build (push) Has been cancelled
The Postiz public API v1 does not expose a media listing endpoint.
Switch URL to the correct internal path (/media?page=0&search=), handle
the resulting 401 with a dedicated lock-icon state, and wire the existing
device gallery picker as an onPickFromDevice fallback so the modal stays
usable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 19:08:15 +02:00
billisdead 0cf5800463 fix(mobile): restore images on repost, improve media library 404 error, null-safe stripHtml
Release APK / build (push) Has been cancelled
- Pass post.image[] as JSON prefillImages param when prefilling compose from an existing post (repost/edit/retry), replacing the broken single-image prefillImagePath/prefillImageId approach
- MediaLibraryModal now shows the attempted URL and a clear explanation on 404, making it easier to diagnose if the Postiz version does not expose GET /media
- stripHtml accepts null/undefined input and returns "" instead of throwing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 08:31:34 +02:00
4 changed files with 66 additions and 19 deletions
+19 -8
View File
@@ -68,12 +68,11 @@ export default function ComposeScreen() {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { workspaces, clients, isConfigured } = usePostiz(); const { workspaces, clients, isConfigured } = usePostiz();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { prefillContent, prefillIntegrationIds, prefillImagePath, prefillImageId } = const { prefillContent, prefillIntegrationIds, prefillImages } =
useLocalSearchParams<{ useLocalSearchParams<{
prefillContent?: string; prefillContent?: string;
prefillIntegrationIds?: string; prefillIntegrationIds?: string;
prefillImagePath?: string; prefillImages?: string;
prefillImageId?: string;
}>(); }>();
const [content, setContent] = useState(""); const [content, setContent] = useState("");
@@ -95,12 +94,23 @@ export default function ComposeScreen() {
if (prefillIntegrationIds) { if (prefillIntegrationIds) {
setSelectedChannels(String(prefillIntegrationIds).split(",").filter(Boolean)); setSelectedChannels(String(prefillIntegrationIds).split(",").filter(Boolean));
} }
if (prefillImagePath && prefillImageId) { if (prefillImages && workspaces.length > 0) {
// Prefilled image has unknown workspace; associate with first workspace try {
const wsId = workspaces[0]?.id ?? ""; const images: Array<{ id: string; path: string }> = JSON.parse(String(prefillImages));
setMediaItems([{ type: "uploaded", id: String(prefillImageId), path: String(prefillImagePath), workspaceId: wsId }]); 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(() => { useEffect(() => {
if (prefillContent) return; if (prefillContent) return;
@@ -757,6 +767,7 @@ export default function ComposeScreen() {
workspaces={workspaces} workspaces={workspaces}
maxSelect={MAX_IMAGES - mediaItems.length} maxSelect={MAX_IMAGES - mediaItems.length}
onClose={() => setShowMediaLibrary(false)} onClose={() => setShowMediaLibrary(false)}
onPickFromDevice={() => { setShowMediaLibrary(false); pickImage(); }}
onSelect={(items: LibraryMediaItem[]) => { onSelect={(items: LibraryMediaItem[]) => {
setMediaItems((prev) => setMediaItems((prev) =>
[ [
+8 -7
View File
@@ -152,13 +152,14 @@ export default function PostsScreen() {
const handlePrefillCompose = (post: PostizPost) => { const handlePrefillCompose = (post: PostizPost) => {
const integrations = post.integrations ?? (post.integration ? [post.integration] : []); const integrations = post.integrations ?? (post.integration ? [post.integration] : []);
router.push({ const params: Record<string, string> = {
pathname: "/(tabs)/compose", prefillContent: stripHtml(post.content),
params: { prefillIntegrationIds: integrations.map((i) => i.id).join(","),
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) => { const startReschedule = (post: PostizPost) => {
@@ -35,6 +35,7 @@ interface Props {
maxSelect: number; maxSelect: number;
onClose: () => void; onClose: () => void;
onSelect: (items: LibraryMediaItem[]) => void; onSelect: (items: LibraryMediaItem[]) => void;
onPickFromDevice?: () => void;
} }
function resolveUrl(path: string, baseUrl: string): string { function resolveUrl(path: string, baseUrl: string): string {
@@ -43,7 +44,7 @@ function resolveUrl(path: string, baseUrl: string): string {
return `${origin}/${path.replace(/^\//, "")}`; return `${origin}/${path.replace(/^\//, "")}`;
} }
export function MediaLibraryModal({ visible, workspaces, defaultWorkspaceId, maxSelect, onClose, onSelect }: Props) { export function MediaLibraryModal({ visible, workspaces, defaultWorkspaceId, maxSelect, onClose, onSelect, onPickFromDevice }: Props) {
const colors = useColors(); const colors = useColors();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const [activeId, setActiveId] = useState<string>(""); const [activeId, setActiveId] = useState<string>("");
@@ -66,12 +67,22 @@ export function MediaLibraryModal({ visible, workspaces, defaultWorkspaceId, max
if (!activeWorkspace) return; if (!activeWorkspace) return;
setLoading(true); setLoading(true);
setError(null); setError(null);
const apiBase = activeWorkspace.baseUrl.replace(/\/public\/v1$/, "");
const url = `${apiBase}/media?page=0&search=`;
try { try {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const res = await globalThis.fetch(`${activeWorkspace.baseUrl}/media`, { const res = await globalThis.fetch(url, {
headers: { Authorization: activeWorkspace.apiKey }, headers: { Authorization: activeWorkspace.apiKey },
}); });
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) {
if (res.status === 401 || res.status === 403) {
throw new Error("SESSION_REQUIRED");
}
if (res.status === 404) {
throw new Error("ENDPOINT_NOT_FOUND");
}
throw new Error(`HTTP ${res.status}${url}`);
}
const data = await res.json(); const data = await res.json();
const list: RawMediaItem[] = Array.isArray(data) const list: RawMediaItem[] = Array.isArray(data)
? data ? data
@@ -163,6 +174,29 @@ export function MediaLibraryModal({ visible, workspaces, defaultWorkspaceId, max
<View style={styles.centered}> <View style={styles.centered}>
<ActivityIndicator color={colors.primary} size="large" /> <ActivityIndicator color={colors.primary} size="large" />
</View> </View>
) : error === "SESSION_REQUIRED" ? (
<View style={styles.centered}>
<Feather name="lock" size={28} color={colors.mutedForeground} />
<Text style={[styles.errorText, { color: colors.mutedForeground }]}>
{"Media library requires a web session.\nAPI key access is not supported by Postiz."}
</Text>
{onPickFromDevice && (
<TouchableOpacity
onPress={() => { onClose(); onPickFromDevice(); }}
style={[styles.retryBtn, { backgroundColor: colors.primary }]}
activeOpacity={0.8}
>
<Text style={[styles.retryText, { color: colors.primaryForeground }]}>Use device gallery</Text>
</TouchableOpacity>
)}
</View>
) : error === "ENDPOINT_NOT_FOUND" ? (
<View style={styles.centered}>
<Feather name="slash" size={28} color={colors.mutedForeground} />
<Text style={[styles.errorText, { color: colors.mutedForeground }]}>
{"Media library endpoint not found on this server."}
</Text>
</View>
) : error ? ( ) : error ? (
<View style={styles.centered}> <View style={styles.centered}>
<Feather name="alert-circle" size={28} color={colors.error} /> <Feather name="alert-circle" size={28} color={colors.error} />
+2 -1
View File
@@ -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 &lt;p&gt; are also stripped // Decode entities first so encoded tags like &lt;p&gt; are also stripped
let s = html let s = html
.replace(/&amp;/g, "&") .replace(/&amp;/g, "&")