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:
@@ -5,7 +5,8 @@ import * as Haptics from "expo-haptics";
|
||||
import { Image } from "expo-image";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import { fetch as expoFetch } from "expo/fetch";
|
||||
import React, { useState } from "react";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
@@ -29,6 +30,10 @@ export default function ComposeScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { client, isConfigured, apiKey, baseUrl } = usePostiz();
|
||||
const queryClient = useQueryClient();
|
||||
const { prefillContent, prefillIntegrationIds } = useLocalSearchParams<{
|
||||
prefillContent?: string;
|
||||
prefillIntegrationIds?: string;
|
||||
}>();
|
||||
const [content, setContent] = useState("");
|
||||
const [selectedChannels, setSelectedChannels] = useState<string[]>([]);
|
||||
const [postNow, setPostNow] = useState(false);
|
||||
@@ -41,6 +46,13 @@ export default function ComposeScreen() {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (prefillContent) setContent(String(prefillContent));
|
||||
if (prefillIntegrationIds) {
|
||||
setSelectedChannels(String(prefillIntegrationIds).split(",").filter(Boolean));
|
||||
}
|
||||
}, [prefillContent, prefillIntegrationIds]);
|
||||
|
||||
const { data: integrations, isLoading: loadingIntegrations } =
|
||||
useQuery<PostizIntegration[]>({
|
||||
queryKey: ["integrations", !!client],
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user