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:
2026-06-07 22:20:56 +02:00
parent bc0973ccaa
commit 7aacb9a53e
8 changed files with 419 additions and 112 deletions
@@ -18,6 +18,7 @@ interface PostCardProps {
post: PostizPost;
onDelete: (id: string) => Promise<void>;
onLongPress: (post: PostizPost) => void;
onReschedule?: (post: PostizPost) => void;
}
function formatDate(dateStr: string): string {
@@ -44,7 +45,7 @@ function getNetworkIcon(type?: string): React.ComponentProps<typeof Feather>["na
return "globe";
}
export function PostCard({ post, onDelete, onLongPress }: PostCardProps) {
export function PostCard({ post, onDelete, onLongPress, onReschedule }: PostCardProps) {
const colors = useColors();
const swipeRef = useRef<Swipeable>(null);
@@ -88,6 +89,34 @@ export function PostCard({ post, onDelete, onLongPress }: PostCardProps) {
);
};
const renderLeftActions =
post.state === "QUEUE" && onReschedule
? (
_progress: Animated.AnimatedInterpolation<number>,
dragX: Animated.AnimatedInterpolation<number>
) => {
const scale = dragX.interpolate({
inputRange: [0, 80],
outputRange: [0.8, 1],
extrapolate: "clamp",
});
return (
<TouchableOpacity
style={[styles.rescheduleAction, { backgroundColor: colors.warning }]}
onPress={() => {
swipeRef.current?.close();
onReschedule(post);
}}
activeOpacity={0.8}
>
<Animated.View style={{ transform: [{ scale }] }}>
<Feather name="clock" size={20} color="#fff" />
</Animated.View>
</TouchableOpacity>
);
}
: undefined;
const integrations = post.integrations ?? (post.integration ? [post.integration] : []);
const truncatedContent =
post.content.length > 140
@@ -98,7 +127,9 @@ export function PostCard({ post, onDelete, onLongPress }: PostCardProps) {
<Swipeable
ref={swipeRef}
renderRightActions={renderRightActions}
renderLeftActions={renderLeftActions}
rightThreshold={40}
leftThreshold={40}
friction={2}
>
<TouchableOpacity
@@ -218,4 +249,9 @@ const styles = StyleSheet.create({
alignItems: "center",
justifyContent: "center",
},
rescheduleAction: {
width: 72,
alignItems: "center",
justifyContent: "center",
},
});