import { Feather } from "@expo/vector-icons"; import DateTimePicker from "@react-native-community/datetimepicker"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import axios from "axios"; import * as Clipboard from "expo-clipboard"; import * as Haptics from "expo-haptics"; import { useRouter } from "expo-router"; import React, { useMemo, useState } from "react"; import { ActivityIndicator, Alert, FlatList, Platform, RefreshControl, StyleSheet, Text, TouchableOpacity, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { PostCard } from "@/components/PostCard"; import { PostizPost, usePostiz } from "@/context/PostizContext"; import { useColors } from "@/hooks/useColors"; function extractError(err: unknown): string { if (axios.isAxiosError(err)) { const status = err.response?.status; const data = err.response?.data; if (data) { const body = typeof data === "string" ? data.slice(0, 200) : (data?.message ?? data?.error ?? JSON.stringify(data)).toString().slice(0, 200); return status ? `HTTP ${status}: ${body}` : body; } if (status) return `HTTP ${status} — ${err.message}`; if (err.message) return err.message; } if (err instanceof Error) return err.message; return "Unknown error"; } type FilterType = "all" | "QUEUE" | "PUBLISHED" | "ERROR" | "DRAFT"; const FILTERS: { key: FilterType; label: string }[] = [ { key: "all", label: "All" }, { key: "QUEUE", label: "Queue" }, { key: "PUBLISHED", label: "Published" }, { key: "DRAFT", label: "Draft" }, { key: "ERROR", label: "Error" }, ]; export default function PostsScreen() { const colors = useColors(); const insets = useSafeAreaInsets(); const { client, isConfigured } = usePostiz(); const queryClient = useQueryClient(); const router = useRouter(); const [filter, setFilter] = useState("all"); const [refreshing, setRefreshing] = useState(false); // reschedule state const [reschedulePost, setReschedulePost] = useState(null); const [rescheduleDate, setRescheduleDate] = useState(new Date()); const [rescheduleStep, setRescheduleStep] = useState<"date" | "time" | null>(null); const { startDate, endDate } = useMemo(() => { const s = new Date(); s.setMonth(s.getMonth() - 3); const e = new Date(); e.setMonth(e.getMonth() + 6); return { startDate: s.toISOString(), endDate: e.toISOString() }; }, []); const { data: posts, isLoading, error, refetch } = useQuery({ queryKey: ["posts-list", !!client, startDate, endDate], queryFn: async () => { if (!client) return []; const res = await client.get("posts", { params: { startDate, endDate }, }); return Array.isArray(res.data) ? res.data : res.data?.posts ?? []; }, enabled: !!client, retry: 1, staleTime: 0, }); const filteredPosts = filter === "all" ? posts ?? [] : (posts ?? []).filter((p) => p.state === filter); const handleRefresh = async () => { setRefreshing(true); await refetch(); setRefreshing(false); }; const handleDelete = async (id: string) => { if (!client) return; try { await client.delete(`posts/${id}`); queryClient.invalidateQueries({ queryKey: ["posts-list"] }); queryClient.invalidateQueries({ queryKey: ["posts"] }); } catch (e: unknown) { const msg = extractError(e); Alert.alert("Delete failed", msg); } }; const handleRetry = async (post: PostizPost) => { if (!client) return; const integrations = post.integrations ?? (post.integration ? [post.integration] : []); try { const payload = { type: "now", date: new Date().toISOString(), shortLink: false, tags: [] as string[], posts: integrations.map((intg) => ({ integration: { id: intg.id }, value: [{ content: post.content, id: "", image: post.image ?? [] }], })), }; await client.post("posts", payload); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); queryClient.invalidateQueries({ queryKey: ["posts-list"] }); Alert.alert("Retried", "Post submitted again."); } catch (e: unknown) { const msg = extractError(e); Alert.alert("Retry failed", msg); } }; const handlePrefillCompose = (post: PostizPost) => { const integrations = post.integrations ?? (post.integration ? [post.integration] : []); router.push({ pathname: "/(tabs)/compose", params: { prefillContent: post.content, prefillIntegrationIds: integrations.map((i) => i.id).join(","), }, }); }; const startReschedule = (post: PostizPost) => { setReschedulePost(post); setRescheduleDate(new Date(post.publishDate)); setRescheduleStep("date"); }; const submitReschedule = async (post: PostizPost, date: Date) => { if (!client) return; try { await client.put(`posts/${post.id}`, { date: date.toISOString() }); queryClient.invalidateQueries({ queryKey: ["posts-list"] }); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); Alert.alert("Rescheduled", `Post moved to ${date.toLocaleDateString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })}`); } catch (e: unknown) { const msg = extractError(e); Alert.alert("Reschedule failed", msg); } }; const showContextMenu = (post: PostizPost) => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); const preview = post.content.slice(0, 60) + (post.content.length > 60 ? "…" : ""); const buttons: Array<{ text: string; style?: "cancel" | "destructive" | "default"; onPress?: () => void }> = []; buttons.push({ text: "Copy text", onPress: () => Clipboard.setStringAsync(post.content), }); if (post.state === "ERROR") { if (post.errorMessage) { buttons.push({ text: "View error", onPress: () => Alert.alert("Error details", post.errorMessage), }); } buttons.push({ text: "Retry now", onPress: () => handleRetry(post) }); buttons.push({ text: "Edit & retry", onPress: () => handlePrefillCompose(post) }); } else if (post.state === "QUEUE") { buttons.push({ text: "Edit", onPress: () => handlePrefillCompose(post) }); buttons.push({ text: "Reschedule", onPress: () => startReschedule(post) }); } else if (post.state === "PUBLISHED") { buttons.push({ text: "Repost", onPress: () => handlePrefillCompose(post) }); } else if (post.state === "DRAFT") { buttons.push({ text: "Edit & schedule", onPress: () => handlePrefillCompose(post) }); } buttons.push({ text: "Cancel", style: "cancel" }); Alert.alert( post.state === "ERROR" ? "Failed post" : post.state === "QUEUE" ? "Scheduled post" : post.state === "PUBLISHED" ? "Published post" : "Draft", preview, buttons ); }; if (!isConfigured) { return ( Not Configured Add your API key in Settings ); } return ( item.key} showsHorizontalScrollIndicator={false} contentContainerStyle={styles.filterList} renderItem={({ item }) => ( setFilter(item.key)} activeOpacity={0.7} style={[ styles.filterChip, { backgroundColor: filter === item.key ? colors.primary : colors.secondary, borderColor: filter === item.key ? colors.primary : colors.border, }, ]} > {item.label} )} style={[styles.filterBar, { borderBottomColor: colors.border }]} /> {isLoading ? ( ) : error ? ( Failed to load {extractError(error)} refetch()} style={[styles.retryBtn, { backgroundColor: colors.primary }]} > Try Again ) : ( item.id} renderItem={({ item }) => ( )} refreshControl={ } contentInsetAdjustmentBehavior="automatic" showsVerticalScrollIndicator={false} ListEmptyComponent={ No posts {filter === "all" ? "No posts found in the last 3 months" : `No ${filter.toLowerCase()} posts`} } scrollEnabled={filteredPosts.length > 0} /> )} {rescheduleStep !== null && reschedulePost !== null && ( { if (!date) { setRescheduleStep(null); setReschedulePost(null); return; } if (rescheduleStep === "date") { const merged = new Date(rescheduleDate); merged.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()); setRescheduleDate(merged); setRescheduleStep("time"); } else { const merged = new Date(rescheduleDate); merged.setHours(date.getHours(), date.getMinutes()); const post = reschedulePost; setRescheduleStep(null); setReschedulePost(null); submitReschedule(post, merged); } }} /> )} ); } const styles = StyleSheet.create({ container: { flex: 1 }, centered: { flex: 1, alignItems: "center", justifyContent: "center", gap: 10, paddingHorizontal: 32, }, filterBar: { borderBottomWidth: StyleSheet.hairlineWidth, flexGrow: 0 }, filterList: { paddingHorizontal: 16, paddingVertical: 10, gap: 8 }, filterChip: { paddingHorizontal: 14, paddingVertical: 6, borderRadius: 20, borderWidth: 1 }, filterText: { fontSize: 13, fontFamily: "Inter_500Medium" }, emptyState: { alignItems: "center", paddingTop: 64, gap: 10 }, emptyTitle: { fontSize: 18, fontFamily: "Inter_600SemiBold" }, emptyText: { fontSize: 14, fontFamily: "Inter_400Regular", textAlign: "center" }, retryBtn: { marginTop: 4, paddingHorizontal: 20, paddingVertical: 10, borderRadius: 10 }, retryText: { fontSize: 14, fontFamily: "Inter_600SemiBold" }, });