From 9e4c9071e6c1a74dc893dcc2689d3739485b5d73 Mon Sep 17 00:00:00 2001 From: antoinepiron <58579297-antoinepiron@users.noreply.replit.com> Date: Sun, 3 May 2026 11:48:45 +0000 Subject: [PATCH] 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 --- .replit | 12 ++ artifacts/postiz-mobile/app.json | 14 +- artifacts/postiz-mobile/app/_layout.tsx | 7 + .../postiz-mobile/hooks/useNotifications.ts | 145 ++++++++++++++++++ artifacts/postiz-mobile/package.json | 2 + pnpm-lock.yaml | 84 ++++++++++ 6 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 artifacts/postiz-mobile/hooks/useNotifications.ts diff --git a/.replit b/.replit index 3d7fac1..7d84db4 100644 --- a/.replit +++ b/.replit @@ -30,3 +30,15 @@ externalPort = 80 [[ports]] localPort = 20976 externalPort = 3000 + +[userenv] + +[userenv.shared] +GITEA_SSH_KEY = """ +-----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW + QyNTUxOQAAACC0tlU0K92MWgUqdlSp84H531RogDoKAw93QTHtuLWiIQAAAJjbnAM925wD + PQAAAAtzc2gtZWQyNTUxOQAAACC0tlU0K92MWgUqdlSp84H531RogDoKAw93QTHtuLWiIQ + AAAEDLSpi/GbkN3yAw0FSMqPE8G6D7NqHQH5e2yRwC3tqkIbS2VTQr3YxaBSp2VKnzgfnf + VGiAOgoDD3dBMe24taIhAAAAFHJlcGxpdC1wb3N0aXotbW9iaWxlAQ== + -----END OPENSSH PRIVATE KEY-----""" diff --git a/artifacts/postiz-mobile/app.json b/artifacts/postiz-mobile/app.json index e0c83a0..92771d3 100644 --- a/artifacts/postiz-mobile/app.json +++ b/artifacts/postiz-mobile/app.json @@ -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, diff --git a/artifacts/postiz-mobile/app/_layout.tsx b/artifacts/postiz-mobile/app/_layout.tsx index acde92a..61683ce 100644 --- a/artifacts/postiz-mobile/app/_layout.tsx +++ b/artifacts/postiz-mobile/app/_layout.tsx @@ -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 ( @@ -56,6 +62,7 @@ export default function RootLayout() { + diff --git a/artifacts/postiz-mobile/hooks/useNotifications.ts b/artifacts/postiz-mobile/hooks/useNotifications.ts new file mode 100644 index 0000000..fcc9ae0 --- /dev/null +++ b/artifacts/postiz-mobile/hooks/useNotifications.ts @@ -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> { + 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) { + 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 | 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 = { ...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 }; +} diff --git a/artifacts/postiz-mobile/package.json b/artifacts/postiz-mobile/package.json index 58e8126..56b5078 100644 --- a/artifacts/postiz-mobile/package.json +++ b/artifacts/postiz-mobile/package.json @@ -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" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 746fb2f..e62ac8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -404,9 +404,15 @@ importers: axios: specifier: ^1.15.2 version: 1.15.2 + expo-notifications: + specifier: ^55.0.22 + version: 55.0.22(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) expo-secure-store: specifier: ^55.0.13 version: 55.0.13(expo@54.0.34) + expo-task-manager: + specifier: ^55.0.15 + version: 55.0.15(expo@54.0.34)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)) react-native-calendars: specifier: ^1.1314.0 version: 1.1314.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -1188,6 +1194,10 @@ packages: '@expo/env@2.0.11': resolution: {integrity: sha512-xV+ps6YCW7XIPVUwFVCRN2nox09dnRwy8uIjwHWTODu0zFw4kp4omnVkl0OOjuu2XOe7tdgAHxikrkJt9xB/7Q==} + '@expo/env@2.1.1': + resolution: {integrity: sha512-rVvHC4I6xlPcg+mAO09ydUi2Wjv1ZytpLmHOSzvXzBAz9mMrJggqCe4s4dubjJvi/Ino/xQCLhbaLCnTtLpikg==} + engines: {node: '>=20.12.0'} + '@expo/fingerprint@0.15.5': resolution: {integrity: sha512-mdVoAMcux1WlM6kd1RoWiHRNqKqS+J6mKmWQ/BKgeh937S/fcW58EE68O6nc4KDXtWi3PBeNHskOFcgyIuD4hw==} hasBin: true @@ -2667,6 +2677,9 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + badgin@1.2.3: + resolution: {integrity: sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -3352,6 +3365,11 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} + expo-application@55.0.14: + resolution: {integrity: sha512-NgqDIt3eCf4aVLp1L6AcEanCYoyJeuBsGrgGSzOIvxAsOvp5X3SYKW3ROgpKUnLQEKMWlzwETpjsUGszcqkk8g==} + peerDependencies: + expo: '*' + expo-asset@12.0.13: resolution: {integrity: sha512-x/p7WvQUnkn6K43b9eL6SPeq5Vnf1E8BDe9bDrWrvMqzyUvJnUFvl+ctg3034s/+UHe7Ne2pAmc0+yzbl8CrDQ==} peerDependencies: @@ -3372,6 +3390,12 @@ packages: expo: '*' react-native: '*' + expo-constants@55.0.15: + resolution: {integrity: sha512-w394fcZLJjeKN+9ZnJzL/HiarE1nwZFDa+3S9frevh6Ur+MAAs9QDrcXhDrV8T3xqRzzYaqsP6Z8TFZ4efWN1A==} + peerDependencies: + expo: '*' + react-native: '*' + expo-file-system@19.0.22: resolution: {integrity: sha512-l9pgahSc7sJD0bP9vBNeXvZjy8QKDpVHVxWmei/ESQOrzmoj5BidziqLVsyZdxsi+PfdbTtttLTAmddH/JafYA==} peerDependencies: @@ -3452,6 +3476,13 @@ packages: react: '*' react-native: '*' + expo-notifications@55.0.22: + resolution: {integrity: sha512-Rwvsp/lAEXfDYBxkQZpaLF9ZB25cJ/yfHhD/ESclbPesN0nbQBZ/5rGb1xS/saANtkStbEGfDlA80uHh2zEpsA==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + expo-router@6.0.23: resolution: {integrity: sha512-qCxVAiCrCyu0npky6azEZ6dJDMt77OmCzEbpF6RbUTlfkaCA417LvY14SBkk0xyGruSxy/7pvJOI6tuThaUVCA==} peerDependencies: @@ -3521,6 +3552,12 @@ packages: react-native-web: optional: true + expo-task-manager@55.0.15: + resolution: {integrity: sha512-wLqYkKBp9cxIonEIp3LYy9iFjlOxxw4ca8nZLdSriKVxzPvdUwX6cZ4g55Fi+uSi4oPVFo9JYFKVUEofc+do+A==} + peerDependencies: + expo: '*' + react-native: '*' + expo-web-browser@15.0.11: resolution: {integrity: sha512-r2LS4Ro6DgUPZkcaEfgt8mp9eJuoA93x11Jh7S6utFe0FEzvUNn2yFhxg8XVwESaaHGt2k5V8LuK36rsp0BeIw==} peerDependencies: @@ -5481,6 +5518,9 @@ packages: resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} engines: {node: '>=20'} + unimodules-app-loader@55.0.5: + resolution: {integrity: sha512-2eLjtaAVQTK3EeiUAgRbfEnX78f6cMtw5Js8Ri4OcEdkrozsmvG3Wu8YVfr6kfhea17FHZkKZmO1m4dL/Ky2Bg==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -6604,6 +6644,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@expo/env@2.1.1': + dependencies: + chalk: 4.1.2 + debug: 4.4.3 + getenv: 2.0.0 + transitivePeerDependencies: + - supports-color + '@expo/fingerprint@0.15.5': dependencies: '@expo/spawn-async': 1.7.2 @@ -8669,6 +8717,8 @@ snapshots: babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + badgin@1.2.3: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -9241,6 +9291,10 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + expo-application@55.0.14(expo@54.0.34): + 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) + expo-asset@12.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))(react@19.1.0)(typescript@5.9.3): dependencies: '@expo/image-utils': 0.8.13(typescript@5.9.3) @@ -9267,6 +9321,14 @@ snapshots: transitivePeerDependencies: - supports-color + expo-constants@55.0.15(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/env': 2.1.1 + 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-native: 0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) + transitivePeerDependencies: + - supports-color + expo-file-system@19.0.22(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: 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) @@ -9345,6 +9407,20 @@ 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-notifications@55.0.22(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): + dependencies: + '@expo/image-utils': 0.8.13(typescript@5.9.3) + abort-controller: 3.0.0 + badgin: 1.2.3 + 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) + expo-application: 55.0.14(expo@54.0.34) + expo-constants: 55.0.15(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 + react-native: 0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) + transitivePeerDependencies: + - supports-color + - typescript + expo-router@6.0.23(@types/react-dom@19.1.11(@types/react@19.1.17))(@types/react@19.1.17)(expo-constants@18.0.13)(expo-linking@8.0.12)(expo@54.0.34)(react-dom@19.1.0(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.7(react-native-worklets@0.5.1(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(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/metro-runtime': 6.1.2(expo@54.0.34)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -9425,6 +9501,12 @@ snapshots: transitivePeerDependencies: - supports-color + expo-task-manager@55.0.15(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: 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-native: 0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) + unimodules-app-loader: 55.0.5 + expo-web-browser@15.0.11(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: 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) @@ -11694,6 +11776,8 @@ snapshots: unicorn-magic@0.4.0: {} + unimodules-app-loader@55.0.5: {} + universalify@2.0.1: {} unpipe@1.0.0: {}