feat: Ability to consume webhook notifications and forward to clients #595

- forward expo device tokens to users plugin instance
- added android notification icon
This commit is contained in:
herrrta
2025-03-10 01:05:51 -04:00
parent cbcb160bdd
commit 9b0ba285b3
5 changed files with 85 additions and 11 deletions

View File

@@ -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"
},

View File

@@ -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<ExpoPushToken>();
const notificationListener = useRef<EventSubscription>();
const responseListener = useRef<EventSubscription>();
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(() => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -13,6 +13,10 @@ declare module "@jellyfin/sdk" {
data: D,
config?: AxiosRequestConfig<D>
): Promise<AxiosResponse<T>>;
delete<T, D = any>(
url: string,
config?: AxiosRequestConfig<D>
): Promise<AxiosResponse<T>>;
getStreamyfinPluginConfig(): Promise<AxiosResponse<StreamyfinPluginConfig>>;
}
}
@@ -32,9 +36,18 @@ Api.prototype.post = function <T, D = any>(
data: D,
config: AxiosRequestConfig<D>
): Promise<AxiosResponse<T>> {
return this.axiosInstance.post<T>(`${this.basePath}${url}`, {
return this.axiosInstance.post<T>(`${this.basePath}${url}`, data, {
...(config || {}),
headers: { [AUTHORIZATION_HEADER]: this.authorizationHeader },
});
};
Api.prototype.delete = function <T, D = any>(
url: string,
config: AxiosRequestConfig<D>
): Promise<AxiosResponse<T>> {
return this.axiosInstance.delete<T>(`${this.basePath}${url}`, {
...(config || {}),
data,
headers: { [AUTHORIZATION_HEADER]: this.authorizationHeader },
});
};

View File

@@ -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);