feat: add long-press contextual actions on post cards

Long press any post card to open a context menu with state-aware actions:
- Copy text (all states)
- ERROR: Retry now, Edit & retry, View error message
- QUEUE: Edit, Reschedule (native DateTimePicker → PUT /posts/:id)
- PUBLISHED: Repost
- DRAFT: Edit & schedule

Compose screen now accepts prefillContent/prefillIntegrationIds router
params to pre-fill content and channel selection when editing or reposting.
Adds expo-clipboard for clipboard support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 21:52:07 +02:00
parent 803f147fbb
commit 3191691fff
7 changed files with 190 additions and 10 deletions
+141 -1
View File
@@ -1,6 +1,10 @@
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,
@@ -51,9 +55,15 @@ export default function PostsScreen() {
const insets = useSafeAreaInsets();
const { client, isConfigured } = usePostiz();
const queryClient = useQueryClient();
const router = useRouter();
const [filter, setFilter] = useState<FilterType>("all");
const [refreshing, setRefreshing] = useState(false);
// reschedule state
const [reschedulePost, setReschedulePost] = useState<PostizPost | null>(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);
@@ -99,6 +109,101 @@ export default function PostsScreen() {
}
};
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 (
<View
@@ -193,7 +298,11 @@ export default function PostsScreen() {
data={filteredPosts}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<PostCard post={item} onDelete={handleDelete} />
<PostCard
post={item}
onDelete={handleDelete}
onLongPress={showContextMenu}
/>
)}
refreshControl={
<RefreshControl
@@ -220,6 +329,37 @@ export default function PostsScreen() {
scrollEnabled={filteredPosts.length > 0}
/>
)}
{rescheduleStep !== null && reschedulePost !== null && (
<DateTimePicker
value={rescheduleDate}
mode={rescheduleStep}
display="default"
minimumDate={rescheduleStep === "date" ? new Date() : undefined}
textColor={colors.foreground}
accentColor={colors.primary}
onChange={(_: unknown, date?: Date) => {
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);
}
}}
/>
)}
</View>
);
}