Add post status change notifications and set up Git push
Installs `expo-notifications` and `expo-task-manager` packages. Implements a `useNotifications` hook for requesting permissions, polling for post status changes, and sending local notifications for published or errored posts. Updates `app.json` to include notification permissions and the notification handler. Wires the `useNotifications` hook into the app's root layout. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7b0991ce-c7b8-4c82-9acc-fd3f9e762a01 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 7e5cd315-f570-494a-b5a6-c6ee284a4516 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/86064bd6-c937-4ca5-a5bf-bbef5749fb60/7b0991ce-c7b8-4c82-9acc-fd3f9e762a01/kWnlAIM Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -24,7 +24,9 @@
|
||||
"permissions": [
|
||||
"READ_EXTERNAL_STORAGE",
|
||||
"WRITE_EXTERNAL_STORAGE",
|
||||
"READ_MEDIA_IMAGES"
|
||||
"READ_MEDIA_IMAGES",
|
||||
"RECEIVE_BOOT_COMPLETED",
|
||||
"VIBRATE"
|
||||
]
|
||||
},
|
||||
"web": {
|
||||
@@ -40,7 +42,15 @@
|
||||
"expo-font",
|
||||
"expo-web-browser",
|
||||
"expo-image-picker",
|
||||
"expo-secure-store"
|
||||
"expo-secure-store",
|
||||
[
|
||||
"expo-notifications",
|
||||
{
|
||||
"icon": "./assets/images/icon.png",
|
||||
"color": "#6366F1",
|
||||
"sounds": []
|
||||
}
|
||||
]
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true,
|
||||
|
||||
@@ -15,6 +15,7 @@ import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
|
||||
import { ErrorBoundary } from "@/components/ErrorBoundary";
|
||||
import { PostizProvider } from "@/context/PostizContext";
|
||||
import { useNotifications } from "@/hooks/useNotifications";
|
||||
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
@@ -27,6 +28,11 @@ const queryClient = new QueryClient({
|
||||
},
|
||||
});
|
||||
|
||||
function NotificationBootstrap() {
|
||||
useNotifications();
|
||||
return null;
|
||||
}
|
||||
|
||||
function RootLayoutNav() {
|
||||
return (
|
||||
<Stack screenOptions={{ headerBackTitle: "Back" }}>
|
||||
@@ -56,6 +62,7 @@ export default function RootLayout() {
|
||||
<ErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PostizProvider>
|
||||
<NotificationBootstrap />
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<KeyboardProvider>
|
||||
<RootLayoutNav />
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import * as Notifications from "expo-notifications";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { usePostiz } from "@/context/PostizContext";
|
||||
import { PostizPost } from "@/context/PostizContext";
|
||||
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => ({
|
||||
shouldShowAlert: true,
|
||||
shouldPlaySound: true,
|
||||
shouldSetBadge: true,
|
||||
shouldShowBanner: true,
|
||||
shouldShowList: true,
|
||||
}),
|
||||
});
|
||||
|
||||
const POLL_INTERVAL_MS = 15 * 60 * 1000;
|
||||
const SEEN_KEY = "postiz_seen_statuses";
|
||||
|
||||
async function getSeenStatuses(): Promise<Record<string, string>> {
|
||||
try {
|
||||
const { default: AsyncStorage } = await import(
|
||||
"@react-native-async-storage/async-storage"
|
||||
);
|
||||
const raw = await AsyncStorage.getItem(SEEN_KEY);
|
||||
return raw ? JSON.parse(raw) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSeenStatuses(map: Record<string, string>) {
|
||||
try {
|
||||
const { default: AsyncStorage } = await import(
|
||||
"@react-native-async-storage/async-storage"
|
||||
);
|
||||
await AsyncStorage.setItem(SEEN_KEY, JSON.stringify(map));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function sendStatusNotification(post: PostizPost) {
|
||||
const isError = post.status === "ERROR";
|
||||
await Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: isError ? "Post failed to publish" : "Post published!",
|
||||
body:
|
||||
post.content.length > 80
|
||||
? post.content.slice(0, 80) + "…"
|
||||
: post.content,
|
||||
data: { postId: post.id },
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
}
|
||||
|
||||
export function useNotifications() {
|
||||
const { client, isConfigured } = usePostiz();
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const permissionGranted = useRef(false);
|
||||
|
||||
const requestPermissions = useCallback(async () => {
|
||||
if (Platform.OS === "web") return false;
|
||||
const { status: existing } = await Notifications.getPermissionsAsync();
|
||||
if (existing === "granted") {
|
||||
permissionGranted.current = true;
|
||||
return true;
|
||||
}
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
permissionGranted.current = status === "granted";
|
||||
return permissionGranted.current;
|
||||
}, []);
|
||||
|
||||
const checkForStatusChanges = useCallback(async () => {
|
||||
if (!client || !permissionGranted.current) return;
|
||||
|
||||
try {
|
||||
const now = new Date();
|
||||
const from = new Date(now);
|
||||
from.setDate(from.getDate() - 7);
|
||||
|
||||
const res = await client.get("/posts", {
|
||||
params: {
|
||||
startDate: from.toISOString(),
|
||||
endDate: now.toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
const posts: PostizPost[] = Array.isArray(res.data)
|
||||
? res.data
|
||||
: res.data?.posts ?? [];
|
||||
|
||||
const seen = await getSeenStatuses();
|
||||
const updated: Record<string, string> = { ...seen };
|
||||
const toNotify: PostizPost[] = [];
|
||||
|
||||
for (const post of posts) {
|
||||
const prev = seen[post.id];
|
||||
if (prev === undefined) {
|
||||
updated[post.id] = post.status;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
prev !== post.status &&
|
||||
(post.status === "PUBLISHED" || post.status === "ERROR")
|
||||
) {
|
||||
toNotify.push(post);
|
||||
}
|
||||
updated[post.id] = post.status;
|
||||
}
|
||||
|
||||
await saveSeenStatuses(updated);
|
||||
|
||||
for (const post of toNotify) {
|
||||
await sendStatusNotification(post);
|
||||
}
|
||||
} catch {}
|
||||
}, [client]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConfigured || Platform.OS === "web") return;
|
||||
|
||||
let mounted = true;
|
||||
|
||||
(async () => {
|
||||
const granted = await requestPermissions();
|
||||
if (!granted || !mounted) return;
|
||||
|
||||
await checkForStatusChanges();
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
checkForStatusChanges();
|
||||
}, POLL_INTERVAL_MS);
|
||||
})();
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isConfigured, requestPermissions, checkForStatusChanges]);
|
||||
|
||||
return { requestPermissions };
|
||||
}
|
||||
@@ -58,7 +58,9 @@
|
||||
"dependencies": {
|
||||
"@react-native-community/datetimepicker": "^9.1.0",
|
||||
"axios": "^1.15.2",
|
||||
"expo-notifications": "^55.0.22",
|
||||
"expo-secure-store": "^55.0.13",
|
||||
"expo-task-manager": "^55.0.15",
|
||||
"react-native-calendars": "^1.1314.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user