diff --git a/src/App/App.js b/src/App/App.js
index adab67d3c..f4b9f1b1e 100644
--- a/src/App/App.js
+++ b/src/App/App.js
@@ -6,7 +6,7 @@ const { useTranslation } = require('react-i18next');
const { Router } = require('stremio-router');
const { Core, Shell, Chromecast, Discord, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
const { NotFound } = require('stremio/routes');
-const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, withCoreSuspender, useShell, useBinaryState } = require('stremio/common');
+const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, ShortcutsProvider, DiscordProvider, CONSTANTS, withCoreSuspender, useShell, useBinaryState } = require('stremio/common');
const ServicesToaster = require('./ServicesToaster');
const DeepLinkHandler = require('./DeepLinkHandler');
const SearchParamsHandler = require('./SearchParamsHandler');
@@ -217,18 +217,20 @@ const App = () => {
- {
- shortcutModalOpen &&
- }
-
-
-
-
-
+
+ {
+ shortcutModalOpen &&
+ }
+
+
+
+
+
+
diff --git a/src/common/Discord/Discord.tsx b/src/common/Discord/Discord.tsx
new file mode 100644
index 000000000..5be7b8177
--- /dev/null
+++ b/src/common/Discord/Discord.tsx
@@ -0,0 +1,129 @@
+import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
+import { useServices } from 'stremio/services';
+import useProfile from '../useProfile';
+
+type Activity = {
+ state: string,
+ details?: string | null,
+ image?: string | null,
+ startTimestamp?: number | null,
+};
+
+type DiscordContextValue = {
+ available: boolean,
+ connected: boolean,
+ enabled: boolean,
+ setActivity: (activity: Activity | null) => void,
+};
+
+const DiscordContext = createContext(null);
+
+const sameActivity = (first: Activity | null, second: Activity | null) => {
+ return first?.state === second?.state &&
+ first?.details === second?.details &&
+ first?.image === second?.image &&
+ first?.startTimestamp === second?.startTimestamp;
+};
+
+type Props = {
+ children: React.ReactNode,
+};
+
+const DiscordProvider = ({ children }: Props) => {
+ const { discord } = useServices();
+ const profile = useProfile();
+ const enabled = profile.settings?.discordRpcEnabled === true;
+ const available = discord?.available === true;
+ const [connected, setConnected] = useState(false);
+ const [activity, setActivityState] = useState(null);
+ const sentActivity = useRef(null);
+ const connectRequested = useRef(false);
+
+ useEffect(() => {
+ if (!discord) return;
+
+ const onStatusChanged = (isConnected: boolean) => {
+ connectRequested.current = false;
+ setConnected(isConnected);
+ };
+
+ discord.on('statusChanged', onStatusChanged);
+ return () => {
+ discord.off('statusChanged', onStatusChanged);
+ };
+ }, [discord]);
+
+ useEffect(() => {
+ if (!discord || !available) {
+ connectRequested.current = false;
+ setConnected(false);
+ sentActivity.current = null;
+ return;
+ }
+
+ if (enabled) {
+ if (!connected && !connectRequested.current) {
+ connectRequested.current = true;
+ discord.connect();
+ }
+ } else {
+ connectRequested.current = false;
+ if (connected) {
+ discord.disconnect();
+ }
+ sentActivity.current = null;
+ }
+ }, [available, connected, discord, enabled]);
+
+ useEffect(() => {
+ if (!discord || !available || !enabled || !connected) return;
+
+ if (activity === null) {
+ if (sentActivity.current !== null) {
+ discord.clearActivity();
+ sentActivity.current = null;
+ }
+ return;
+ }
+
+ if (sameActivity(sentActivity.current, activity)) return;
+
+ discord.setActivity(
+ activity.state,
+ activity.details || '',
+ activity.image || null,
+ activity.startTimestamp || null
+ );
+ sentActivity.current = activity;
+ }, [activity, available, connected, discord, enabled]);
+
+ const setActivity = useCallback((nextActivity: Activity | null) => {
+ setActivityState((currentActivity) => sameActivity(currentActivity, nextActivity) ? currentActivity : nextActivity);
+ }, []);
+
+ const value = useMemo(() => ({
+ available,
+ connected,
+ enabled,
+ setActivity,
+ }), [available, connected, enabled, setActivity]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+const useDiscord = () => {
+ const value = useContext(DiscordContext);
+ if (value === null) {
+ throw new Error('useDiscord must be used inside DiscordProvider');
+ }
+ return value;
+};
+
+export {
+ DiscordProvider,
+ useDiscord,
+};
diff --git a/src/common/Discord/index.ts b/src/common/Discord/index.ts
new file mode 100644
index 000000000..f16f4bc47
--- /dev/null
+++ b/src/common/Discord/index.ts
@@ -0,0 +1,6 @@
+import { DiscordProvider, useDiscord } from './Discord';
+
+export {
+ DiscordProvider,
+ useDiscord,
+};
diff --git a/src/common/index.js b/src/common/index.js
index f118ec1a7..e4b2c1f4c 100644
--- a/src/common/index.js
+++ b/src/common/index.js
@@ -5,6 +5,7 @@ const { PlatformProvider, usePlatform } = require('./Platform');
const { ToastProvider, useToast } = require('./Toast');
const { TooltipProvider, Tooltip } = require('./Tooltips');
const { ShortcutsProvider, useShortcuts, onShortcut } = require('./Shortcuts');
+const { DiscordProvider, useDiscord } = require('./Discord');
const CONSTANTS = require('./CONSTANTS');
const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender');
const getVisibleChildrenRange = require('./getVisibleChildrenRange');
@@ -16,7 +17,6 @@ const useAnimationFrame = require('./useAnimationFrame');
const useBinaryState = require('./useBinaryState');
const { default: useFullscreen } = require('./useFullscreen');
const { default: useInterval } = require('./useInterval');
-const { default: useDiscord } = require('./useDiscord');
const useLiveRef = require('./useLiveRef');
const useModelState = require('./useModelState');
const useNotifications = require('./useNotifications');
@@ -44,6 +44,8 @@ module.exports = {
useToast,
TooltipProvider,
Tooltip,
+ DiscordProvider,
+ useDiscord,
CONSTANTS,
withCoreSuspender,
useCoreSuspender,
@@ -56,7 +58,6 @@ module.exports = {
useBinaryState,
useFullscreen,
useInterval,
- useDiscord,
useLiveRef,
useModelState,
useNotifications,
diff --git a/src/common/useDiscord.ts b/src/common/useDiscord.ts
deleted file mode 100644
index f28e5b71d..000000000
--- a/src/common/useDiscord.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright (C) 2017-2025 Smart code 203358507
-
-import { useCallback, useEffect, useState } from 'react';
-import { useServices } from 'stremio/services';
-
-type Service = {
- available: boolean;
- connected: boolean;
- connect: () => void;
- disconnect: () => void;
- setActivity: (state: string, details: string, image?: string, startTimestamp?: number) => void;
- clearActivity: () => void;
- on: (name: string, listener: (data: unknown) => void) => void;
- off: (name: string, listener: (data: unknown) => void) => void;
-};
-
-type Result = {
- available: boolean;
- connected: boolean;
- connect: () => void;
- disconnect: () => void;
- setActivity: (state: string, details: string, image?: string, startTimestamp?: number) => void;
- clearActivity: () => void;
-};
-
-const useDiscord = (): Result => {
- const { discord } = useServices() as { discord?: Service };
- const [connected, setConnected] = useState(discord?.connected ?? false);
-
- useEffect(() => {
- if (!discord) return;
-
- const onStatusChanged = (isConnected: boolean) => {
- setConnected(isConnected);
- };
-
- discord.on('statusChanged', onStatusChanged as (data: unknown) => void);
-
- return () => {
- discord.off('statusChanged', onStatusChanged as (data: unknown) => void);
- };
- }, [discord]);
-
- const connect = useCallback(() => {
- if (discord) {
- discord.connect();
- }
- }, [discord]);
-
- const disconnect = useCallback(() => {
- if (discord) {
- discord.disconnect();
- }
- }, [discord]);
-
- const setActivity = useCallback((
- state: string,
- details: string,
- image?: string,
- startTimestamp?: number
- ) => {
- if (discord) {
- discord.setActivity(state, details, image, startTimestamp);
- }
- }, [discord]);
-
- const clearActivity = useCallback(() => {
- if (discord) {
- discord.clearActivity();
- }
- }, [discord]);
-
- return {
- available: discord?.available ?? false,
- connected,
- connect,
- disconnect,
- setActivity,
- clearActivity
- };
-};
-
-export default useDiscord;
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index ac47789a4..3440f03ae 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -609,20 +609,26 @@ const Player = ({ urlParams, queryParams }) => {
}, [settings.pauseOnMinimize, shell.windowClosed, shell.windowHidden]);
React.useEffect(() => {
- if (!discord.connected || !discord.available) return;
+ if (video.state.stream === null || typeof player?.title !== 'string') {
+ discord.setActivity(null);
+ return;
+ }
- const state = video.state.paused ? 'Paused' : 'Watching';
-
- const startTimestamp = !video.state.paused && video.state.time !== null && video.state.duration !== null
- ? Math.floor((Date.now() / 1000) - video.state.time)
- : null;
-
- discord.setActivity(state, player?.title, player?.metaItem?.poster, startTimestamp);
+ discord.setActivity({
+ state: video.state.paused ? 'Paused' : 'Watching',
+ details: player.title,
+ image: player.metaItem?.poster || null,
+ startTimestamp: !video.state.paused && typeof video.state.time === 'number' ?
+ Math.floor((Date.now() / 1000) - video.state.time) :
+ null,
+ });
+ }, [discord.setActivity, player?.title, player.metaItem?.poster, video.state.paused, video.state.stream]);
+ React.useEffect(() => {
return () => {
- discord.clearActivity();
+ discord.setActivity(null);
};
- }, [discord.connected, discord.available, player?.title, player?.metaItem, video.state]);
+ }, [discord.setActivity]);
useMediaSession(video.state, player, onPlayRequested, onPauseRequested, onNextVideoRequested);
diff --git a/src/routes/Settings/General/General.less b/src/routes/Settings/General/General.less
index 6b0afc03a..6f8ce9bfb 100644
--- a/src/routes/Settings/General/General.less
+++ b/src/routes/Settings/General/General.less
@@ -12,6 +12,6 @@
.discord-container {
.option-icon {
- color: #5865F2 !important;
+ color: #5865f2 !important;
}
}
diff --git a/src/routes/Settings/General/General.tsx b/src/routes/Settings/General/General.tsx
index 2273aeb80..a31052113 100644
--- a/src/routes/Settings/General/General.tsx
+++ b/src/routes/Settings/General/General.tsx
@@ -17,7 +17,7 @@ const General = forwardRef(({ profile }: Props, ref) => {
const { core } = useServices();
const platform = usePlatform();
const toast = useToast();
- const { available: discordAvailable, connected: isDiscordConnected, connect: connectDiscord, disconnect: disconnectDiscord } = useDiscord();
+ const discord = useDiscord();
const [dataExport, loadDataExport] = useDataExport();
const [traktAuthStarted, setTraktAuthStarted] = useState(false);
@@ -63,30 +63,17 @@ const General = forwardRef(({ profile }: Props, ref) => {
}, [isTraktAuthenticated, profile.auth]);
const onToggleDiscord = useCallback(() => {
- if (isDiscordConnected) {
- disconnectDiscord();
- core.transport.dispatch({
- action: 'Ctx',
+ core.transport.dispatch({
+ action: 'Ctx',
+ args: {
+ action: 'UpdateSettings',
args: {
- action: 'UpdateSettings',
- args: {
- discordRpcEnabled: false
- }
+ ...profile.settings,
+ discordRpcEnabled: !profile.settings.discordRpcEnabled
}
- });
- } else {
- connectDiscord();
- core.transport.dispatch({
- action: 'Ctx',
- args: {
- action: 'UpdateSettings',
- args: {
- discordRpcEnabled: true
- }
- }
- });
- }
- }, [isDiscordConnected, connectDiscord, disconnectDiscord]);
+ }
+ });
+ }, [profile.settings]);
useEffect(() => {
if (dataExport.exportUrl) {
@@ -94,12 +81,6 @@ const General = forwardRef(({ profile }: Props, ref) => {
}
}, [dataExport.exportUrl]);
- useEffect(() => {
- if (discordAvailable && profile.settings.discordRpcEnabled && !isDiscordConnected) {
- connectDiscord();
- }
- }, [discordAvailable, profile.settings.discordRpcEnabled]);
-
useEffect(() => {
if (isTraktAuthenticated && traktAuthStarted) {
core.transport.dispatch({
@@ -168,10 +149,10 @@ const General = forwardRef(({ profile }: Props, ref) => {
{
- discordAvailable &&
-