diff --git a/app.json b/app.json index 8867d0cc..ba4584d5 100644 --- a/app.json +++ b/app.json @@ -120,6 +120,13 @@ "image": "./assets/images/StreamyFinFinal.png", "imageWidth": 100 } + ], + [ + "expo-notifications", + { + "icon": "./assets/images/notification.png", + "color": "#9333EA" + } ] ], "experiments": { @@ -133,7 +140,7 @@ "projectId": "e79219d1-797f-4fbe-9fa1-cfd360690a68" } }, - "owner": "fredrikburmester", + "owner": "streamyfin", "runtimeVersion": { "policy": "appVersion" }, diff --git a/app/_layout.tsx b/app/_layout.tsx index 8dfe0786..2d1ab164 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -12,7 +12,7 @@ import { BACKGROUND_FETCH_TASK_SESSIONS, registerBackgroundFetchAsyncSessions, } from "@/utils/background-tasks"; -import { LogProvider, writeToLog } from "@/utils/log"; +import {LogProvider, writeErrorLog, writeToLog} from "@/utils/log"; import { storage } from "@/utils/mmkv"; import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server"; import { ActionSheetProvider } from "@expo/react-native-action-sheet"; @@ -30,7 +30,7 @@ import * as ScreenOrientation from "@/packages/expo-screen-orientation"; const TaskManager = !Platform.isTV ? require("expo-task-manager") : null; import { getLocales } from "expo-localization"; import { Provider as JotaiProvider } from "jotai"; -import { useEffect, useRef } from "react"; +import {useEffect, useRef, useState} from "react"; import { I18nextProvider } from "react-i18next"; import { Appearance, AppState } from "react-native"; import { SystemBars } from "react-native-edge-to-edge"; @@ -41,6 +41,9 @@ import { useAtom } from "jotai"; import { userAtom } from "@/providers/JellyfinProvider"; import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; import { store } from "@/utils/store"; +import {EventSubscription} from "expo-modules-core"; +import {ExpoPushToken} from "expo-notifications/build/Tokens.types"; +import {Notification, NotificationResponse} from "expo-notifications/build/Notifications.types"; if (!Platform.isTV) { Notifications.setNotificationHandler({ @@ -258,6 +261,7 @@ const queryClient = new QueryClient({ function Layout() { const [settings] = useSettings(); const [user] = useAtom(userAtom); + const [api] = useAtom(apiAtom); const appState = useRef(AppState.currentState); const segments = useSegments(); @@ -268,13 +272,58 @@ function Layout() { if (!Platform.isTV) { useNotificationObserver(); + const [expoPushToken, setExpoPushToken] = useState(); + const notificationListener = useRef(); + const responseListener = useRef(); + useEffect(() => { - checkAndRequestPermissions(); - (async () => { - if (!Platform.isTV && user && user.Policy?.IsAdministrator) { - registerBackgroundFetchAsyncSessions(); - } - })(); + if (expoPushToken && api && user) { + api?.post("/Streamyfin/device", { + token: expoPushToken.data, + deviceId: getOrSetDeviceId(), + userId: user.Id + }).then(_ => console.log("Posted expo push token")) + .catch(_ => writeErrorLog("Failed to push expo push token to plugin")) + } + else console.log("No token available") + }, [api, expoPushToken, user]); + + async function registerNotifications() { + if (Platform.OS === 'android') { + console.log("Setting android notification channel 'default'") + await Notifications?.setNotificationChannelAsync('default', { + name: 'default' + }); + } + + await checkAndRequestPermissions(); + + if (!Platform.isTV && user && user.Policy?.IsAdministrator) { + await registerBackgroundFetchAsyncSessions(); + } + + Notifications?.getExpoPushTokenAsync() + .then((token: ExpoPushToken) => token && setExpoPushToken(token)) + .catch((reason: any) => console.log("Failed to get token", reason)); + } + + useEffect(() => { + registerNotifications() + + notificationListener.current = Notifications?.addNotificationReceivedListener((notification: Notification) => { + console.log("Notification received while app running", notification); + }); + + responseListener.current = Notifications?.addNotificationResponseReceivedListener((response: NotificationResponse) => { + console.log("Notification interacted with", response); + }); + + return () => { + notificationListener.current && + Notifications?.removeNotificationSubscription(notificationListener.current); + responseListener.current && + Notifications?.removeNotificationSubscription(responseListener.current); + } }, []); useEffect(() => { diff --git a/assets/images/notification.png b/assets/images/notification.png new file mode 100644 index 00000000..b50e56ae Binary files /dev/null and b/assets/images/notification.png differ diff --git a/augmentations/api.ts b/augmentations/api.ts index da5c02a9..fa20552d 100644 --- a/augmentations/api.ts +++ b/augmentations/api.ts @@ -13,6 +13,10 @@ declare module "@jellyfin/sdk" { data: D, config?: AxiosRequestConfig ): Promise>; + delete( + url: string, + config?: AxiosRequestConfig + ): Promise>; getStreamyfinPluginConfig(): Promise>; } } @@ -32,9 +36,18 @@ Api.prototype.post = function ( data: D, config: AxiosRequestConfig ): Promise> { - return this.axiosInstance.post(`${this.basePath}${url}`, { + return this.axiosInstance.post(`${this.basePath}${url}`, data, { + ...(config || {}), + headers: { [AUTHORIZATION_HEADER]: this.authorizationHeader }, + }); +}; + +Api.prototype.delete = function ( + url: string, + config: AxiosRequestConfig +): Promise> { + return this.axiosInstance.delete(`${this.basePath}${url}`, { ...(config || {}), - data, headers: { [AUTHORIZATION_HEADER]: this.authorizationHeader }, }); }; diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 37b52346..1256d2ae 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -25,6 +25,7 @@ import { useTranslation } from "react-i18next"; import { Platform } from "react-native"; import { getDeviceName } from "react-native-device-info"; import uuid from "react-native-uuid"; +import {writeErrorLog, writeInfoLog} from "@/utils/log"; interface Server { address: string; @@ -286,6 +287,10 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ const logoutMutation = useMutation({ mutationFn: async () => { + api?.delete(`/Streamyfin/device/${deviceId}`) + .then(r => writeInfoLog("Deleted expo push token for device")) + .catch(e => writeErrorLog(`Failed to delete expo push token for device`)) + storage.delete("token"); setUser(null); setApi(null);