Files
Postiz-android/artifacts/postiz-mobile/hooks/useNotifications.ts
T
billisdead 0696f5663e
Release APK / build (push) Has been cancelled
feat: multi-images, media library, + fix HTML in notifications
Multi-images (compose):
- Replace single imageUri with mediaItems: MediaItem[] (local | uploaded)
- allowsMultipleSelection: true, selectionLimit up to 4 total
- Each picked image is resized to max 1920px before upload
- Thumbnail row with individual × remove buttons
- uploaded badge (cloud icon) on library/prefill images
- buildMediaPayload() uploads local items, passes uploaded items as-is

Media Library:
- New MediaLibraryModal component — full-screen modal
- Fetches GET /media from Postiz instance
- 3-column grid with multi-select (capped at remaining slots)
- Selected items added to compose media pool

Notifications:
- Strip HTML from notification body text

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 17:09:08 +02:00

148 lines
4.5 KiB
TypeScript

import { useCallback, useEffect, useRef } from "react";
import { Platform } from "react-native";
import { usePostiz } from "@/context/PostizContext";
import { PostizPost } from "@/context/PostizContext";
import { stripHtml } from "@/lib/stripHtml";
const POLL_INTERVAL_MS = 15 * 60 * 1000;
const SEEN_KEY = "postiz_seen_statuses";
function isExpoGo(): boolean {
try {
const Constants = require("expo-constants").default;
return Constants?.executionEnvironment === "storeClient";
} catch {
return false;
}
}
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) {
if (Platform.OS === "web" || isExpoGo()) return;
try {
const Notifications = require("expo-notifications");
const isError = post.state === "ERROR";
await Notifications.scheduleNotificationAsync({
content: {
title: isError ? "Post failed to publish" : "Post published!",
body: (() => { const t = stripHtml(post.content); return t.length > 80 ? t.slice(0, 80) + "…" : t; })(),
data: { postId: post.id },
},
trigger: null,
});
} catch {}
}
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" || isExpoGo()) return false;
try {
const Notifications = require("expo-notifications");
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
shouldShowBanner: true,
shouldShowList: true,
}),
});
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;
} catch {
return false;
}
}, []);
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.state;
continue;
}
if (
prev !== post.state &&
(post.state === "PUBLISHED" || post.state === "ERROR")
) {
toNotify.push(post);
}
updated[post.id] = post.state;
}
await saveSeenStatuses(updated);
for (const post of toNotify) {
await sendStatusNotification(post);
}
} catch {}
}, [client]);
useEffect(() => {
if (!isConfigured || Platform.OS === "web" || isExpoGo()) 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 };
}