Compare commits
2 Commits
69b94ab7c0
...
3191691fff
| Author | SHA1 | Date | |
|---|---|---|---|
| 3191691fff | |||
| 803f147fbb |
@@ -23,23 +23,19 @@
|
||||
"android": {
|
||||
"package": "fr.gyozamancave.postizmobile",
|
||||
"permissions": [
|
||||
"READ_EXTERNAL_STORAGE",
|
||||
"WRITE_EXTERNAL_STORAGE",
|
||||
"READ_MEDIA_IMAGES",
|
||||
"RECEIVE_BOOT_COMPLETED",
|
||||
"VIBRATE"
|
||||
"android.permission.READ_EXTERNAL_STORAGE",
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||
"android.permission.READ_MEDIA_IMAGES",
|
||||
"android.permission.RECEIVE_BOOT_COMPLETED",
|
||||
"android.permission.VIBRATE",
|
||||
"android.permission.RECORD_AUDIO"
|
||||
]
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/images/icon.png"
|
||||
},
|
||||
"plugins": [
|
||||
[
|
||||
"expo-router",
|
||||
{
|
||||
"origin": "https://replit.com/"
|
||||
}
|
||||
],
|
||||
"expo-router",
|
||||
"expo-font",
|
||||
"expo-web-browser",
|
||||
"expo-image-picker",
|
||||
@@ -56,6 +52,11 @@
|
||||
"experiments": {
|
||||
"typedRoutes": true,
|
||||
"reactCompiler": true
|
||||
},
|
||||
"extra": {
|
||||
"eas": {
|
||||
"projectId": "aeaaa2bd-3a27-4771-8e39-f2e14fe0e030"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { StatusBadge } from "./StatusBadge";
|
||||
interface PostCardProps {
|
||||
post: PostizPost;
|
||||
onDelete: (id: string) => Promise<void>;
|
||||
onLongPress: (post: PostizPost) => void;
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
@@ -43,7 +44,7 @@ function getNetworkIcon(type?: string): React.ComponentProps<typeof Feather>["na
|
||||
return "globe";
|
||||
}
|
||||
|
||||
export function PostCard({ post, onDelete }: PostCardProps) {
|
||||
export function PostCard({ post, onDelete, onLongPress }: PostCardProps) {
|
||||
const colors = useColors();
|
||||
const swipeRef = useRef<Swipeable>(null);
|
||||
|
||||
@@ -100,7 +101,10 @@ export function PostCard({ post, onDelete }: PostCardProps) {
|
||||
rightThreshold={40}
|
||||
friction={2}
|
||||
>
|
||||
<View
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.85}
|
||||
onLongPress={() => onLongPress(post)}
|
||||
delayLongPress={400}
|
||||
style={[
|
||||
styles.card,
|
||||
{ backgroundColor: colors.card, borderBottomColor: colors.border },
|
||||
@@ -140,7 +144,7 @@ export function PostCard({ post, onDelete }: PostCardProps) {
|
||||
{formatDate(post.publishDate)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Swipeable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface PostizPost {
|
||||
integrations?: PostizIntegration[];
|
||||
image?: PostizMediaItem[];
|
||||
group?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface PostizUploadResult {
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"dependencies": {
|
||||
"@react-native-community/datetimepicker": "8.4.4",
|
||||
"axios": "^1.15.2",
|
||||
"expo-clipboard": "~8.0.8",
|
||||
"expo-notifications": "~0.32.17",
|
||||
"expo-secure-store": "~15.0.8",
|
||||
"expo-task-manager": "~14.0.9",
|
||||
|
||||
Generated
+16
@@ -398,6 +398,9 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.15.2
|
||||
version: 1.15.2
|
||||
expo-clipboard:
|
||||
specifier: ~8.0.8
|
||||
version: 8.0.8(expo@54.0.34)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
|
||||
expo-notifications:
|
||||
specifier: ~0.32.17
|
||||
version: 0.32.17(expo@54.0.34)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(typescript@5.9.3)
|
||||
@@ -3384,6 +3387,13 @@ packages:
|
||||
react: '*'
|
||||
react-native: '*'
|
||||
|
||||
expo-clipboard@8.0.8:
|
||||
resolution: {integrity: sha512-VKoBkHIpZZDJTB0jRO4/PZskHdMNOEz3P/41tmM6fDuODMpqhvyWK053X0ebspkxiawJX9lX33JXHBCvVsTTOA==}
|
||||
peerDependencies:
|
||||
expo: '*'
|
||||
react: '*'
|
||||
react-native: '*'
|
||||
|
||||
expo-constants@18.0.13:
|
||||
resolution: {integrity: sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==}
|
||||
peerDependencies:
|
||||
@@ -9386,6 +9396,12 @@ snapshots:
|
||||
react: 19.1.0
|
||||
react-native: 0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
|
||||
|
||||
expo-clipboard@8.0.8(expo@54.0.34)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
expo: 54.0.34(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(typescript@5.9.3)
|
||||
react: 19.1.0
|
||||
react-native: 0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
|
||||
|
||||
expo-constants@18.0.13(expo@54.0.34)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)):
|
||||
dependencies:
|
||||
'@expo/config': 12.0.13
|
||||
|
||||
Reference in New Issue
Block a user