feat: UX improvements, security hardening, and code cleanup
- Extract shared extractError utility (lib/extractError.ts), remove 3 duplicate copies - Export DEFAULT_BASE_URL from PostizContext, remove duplicate in settings - Add 401 interceptor in axios client: fires UnauthorizedHandler in _layout → alert + redirect to Settings - Calendar day items now tappable: tap opens context menu (Copy / Edit / Repost) - Persist sort order (newest/oldest) across sessions via AsyncStorage - Filter chips show post count per status (Queue 3, Error 1, etc.) - Copy text action now shows a brief "Copied" toast + haptic feedback - PostCard: swipe right → Reschedule action (QUEUE posts only, amber color) - Compose: per-network char limit (Twitter 280, Instagram 2200…) with color warning at 90% - Compose: local draft save/restore via AsyncStorage with restore banner on open - Compose: prefillImagePath/prefillImageId params allow Edit/Repost to carry over existing media Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { router } from "expo-router";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
FlatList,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
@@ -17,24 +19,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { PostizPost, usePostiz } from "@/context/PostizContext";
|
||||
import { useColors } from "@/hooks/useColors";
|
||||
import { StatusBadge } from "@/components/StatusBadge";
|
||||
|
||||
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";
|
||||
}
|
||||
import { extractError } from "@/lib/extractError";
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
const y = date.getFullYear();
|
||||
@@ -65,6 +50,39 @@ export default function CalendarScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { client, isConfigured } = usePostiz();
|
||||
|
||||
const showContextMenu = (post: PostizPost) => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
const preview = post.content.slice(0, 60) + (post.content.length > 60 ? "…" : "");
|
||||
const integrations = post.integrations ?? (post.integration ? [post.integration] : []);
|
||||
Alert.alert(
|
||||
post.state === "PUBLISHED" ? "Published post" :
|
||||
post.state === "QUEUE" ? "Scheduled post" :
|
||||
post.state === "ERROR" ? "Failed post" : "Draft",
|
||||
preview,
|
||||
[
|
||||
{
|
||||
text: "Copy text",
|
||||
onPress: () => {
|
||||
Clipboard.setStringAsync(post.content);
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: post.state === "PUBLISHED" ? "Repost" : "Edit",
|
||||
onPress: () =>
|
||||
router.push({
|
||||
pathname: "/(tabs)/compose",
|
||||
params: {
|
||||
prefillContent: post.content,
|
||||
prefillIntegrationIds: integrations.map((i) => i.id).join(","),
|
||||
},
|
||||
}),
|
||||
},
|
||||
{ text: "Cancel", style: "cancel" },
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
const [currentMonth, setCurrentMonth] = useState({
|
||||
year: now.getFullYear(),
|
||||
@@ -255,6 +273,7 @@ export default function CalendarScreen() {
|
||||
<TouchableOpacity
|
||||
style={[styles.dayPost, { borderBottomColor: colors.border }]}
|
||||
activeOpacity={0.7}
|
||||
onPress={() => showContextMenu(item)}
|
||||
>
|
||||
<View style={styles.dayPostLeft}>
|
||||
<Text style={[styles.timeText, { color: colors.primary }]}>
|
||||
|
||||
Reference in New Issue
Block a user