Last.fm: Reorganize everything
This commit is contained in:
parent
92d511c7b2
commit
85ef273cde
4 changed files with 292 additions and 264 deletions
|
|
@ -1,286 +1,50 @@
|
|||
import { patcher, plugin } from "@vendetta";
|
||||
import { plugin } from "@vendetta";
|
||||
import { findByProps, findByStoreName } from '@vendetta/metro';
|
||||
import { FluxDispatcher } from '@vendetta/metro/common';
|
||||
import Constants from "./constants";
|
||||
import Settings from "./settings";
|
||||
|
||||
let stopped: Boolean,
|
||||
lastActivity: Activity,
|
||||
updateInterval: NodeJS.Timer,
|
||||
lastTrackUrl: string,
|
||||
activityTypeUnpatch: () => void;
|
||||
import Settings from "./Settings";
|
||||
import { flush, initialize, patchActivityType } from "./utils";
|
||||
|
||||
// Discord activity
|
||||
type Activity = {
|
||||
name: string;
|
||||
type: number;
|
||||
details: string;
|
||||
state: string;
|
||||
timestamps?: {
|
||||
start: number;
|
||||
end?: number;
|
||||
};
|
||||
assets?: ActivityAssets;
|
||||
}
|
||||
|
||||
type ResultActivity = {
|
||||
name: string;
|
||||
type: number;
|
||||
details: string;
|
||||
state: string;
|
||||
timestamps?: {
|
||||
start: number;
|
||||
end?: number;
|
||||
};
|
||||
assets?: ActivityAssets;
|
||||
application_id: string;
|
||||
};
|
||||
|
||||
type ActivityAssets = {
|
||||
large_image: string;
|
||||
large_text?: string;
|
||||
} | {
|
||||
small_image: string;
|
||||
small_text?: string;
|
||||
};
|
||||
|
||||
type Track = {
|
||||
name: string;
|
||||
artist: string;
|
||||
album: string;
|
||||
albumArt: string;
|
||||
url: string;
|
||||
date: string;
|
||||
nowPlaying: boolean;
|
||||
loved: boolean;
|
||||
export const global = {} as {
|
||||
stopped?: Boolean,
|
||||
lastActivity?: Activity,
|
||||
updateInterval?: NodeJS.Timer,
|
||||
lastTrackUrl?: string,
|
||||
activityTypeUnpatch?: () => boolean
|
||||
}
|
||||
|
||||
export const currentSettings = { ...plugin.storage } as PluginSettings;
|
||||
|
||||
// Discord modules that we need
|
||||
const UserStore = findByStoreName("UserStore")
|
||||
const SET_ACTIVITY = findByProps('SET_ACTIVITY').SET_ACTIVITY;
|
||||
export const UserStore = findByStoreName("UserStore")
|
||||
export const SET_ACTIVITY = findByProps('SET_ACTIVITY').SET_ACTIVITY;
|
||||
|
||||
const verboseLog = (...message: any) => currentSettings.verboseLogging && console.log(...message);
|
||||
|
||||
async function fetchCurrentScrobble(): Promise<Track> {
|
||||
const params = new URLSearchParams({
|
||||
'method': 'user.getrecenttracks',
|
||||
'user': currentSettings.username,
|
||||
'api_key': Constants.LFM_API_KEY,
|
||||
'format': 'json',
|
||||
'limit': '1',
|
||||
'extended': '1'
|
||||
}).toString();
|
||||
|
||||
const info = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`).then(x => x.json());
|
||||
const lastTrack = info.recenttracks?.track?.[0] || Promise.reject(info);
|
||||
return {
|
||||
name: lastTrack.name,
|
||||
artist: lastTrack.artist.name,
|
||||
album: lastTrack.album['#text'],
|
||||
albumArt: filterAlbumCover(lastTrack.image[3]['#text']),
|
||||
url: lastTrack.url,
|
||||
date: lastTrack.date?.['#text'] ?? 'now',
|
||||
nowPlaying: Boolean(lastTrack['@attr']?.nowplaying),
|
||||
loved: lastTrack.loved === '1',
|
||||
} as Track;
|
||||
}
|
||||
|
||||
// Currently ditches the default album covers (they're ugly)
|
||||
// Might as well use Youtube as fallback too (soontm)
|
||||
function filterAlbumCover(cover: string): string {
|
||||
if (Constants.DEFAULT_COVER_HASHES.some(x => cover.includes(x))) {
|
||||
return null;
|
||||
}
|
||||
return cover;
|
||||
}
|
||||
|
||||
// Clears the activity
|
||||
function clearActivity(): Promise<ResultActivity> {
|
||||
lastActivity && verboseLog("--> Clearing activity...");
|
||||
return sendRequest(null);
|
||||
}
|
||||
|
||||
// Sends the activity details to Discord
|
||||
async function sendRequest(activity: Activity): Promise<ResultActivity> {
|
||||
if (stopped) {
|
||||
console.log("--> Plugin is unloaded, aborting...");
|
||||
updateInterval && clearInterval(updateInterval);
|
||||
activity = null;
|
||||
}
|
||||
|
||||
lastActivity = activity;
|
||||
|
||||
return await SET_ACTIVITY.handler({
|
||||
isSocketConnected: () => true,
|
||||
socket: {
|
||||
id: 120,
|
||||
application: {
|
||||
id: Constants.APPLICATION_ID,
|
||||
name: activity?.name || "RichPresence"
|
||||
},
|
||||
transport: "ipc"
|
||||
},
|
||||
args: {
|
||||
pid: 120,
|
||||
activity: activity && { ...activity } || null
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetches the current scrobble and sets the activity
|
||||
// This is the main function that is called every 5 seconds (or whatever the user has set)
|
||||
export async function update() {
|
||||
verboseLog("--> Fetching last track...");
|
||||
const lastTrack = await fetchCurrentScrobble().catch((err) => {
|
||||
verboseLog("--> An error occurred while fetching the last track, aborting...");
|
||||
clearActivity();
|
||||
throw err;
|
||||
});
|
||||
|
||||
if (!lastTrack.nowPlaying) {
|
||||
verboseLog("--> Last track is not currently playing, aborting...");
|
||||
return await clearActivity();
|
||||
}
|
||||
|
||||
verboseLog("--> Setting activity...");
|
||||
|
||||
if (lastTrackUrl === lastTrack.url) {
|
||||
verboseLog("--> Last track is the same as the previous one, aborting...");
|
||||
return;
|
||||
}
|
||||
|
||||
const activity = {
|
||||
name: currentSettings.appName || "Music",
|
||||
type: currentSettings.listeningTo ? 2 : 0,
|
||||
details: lastTrack.name,
|
||||
state: `by ${lastTrack.artist}`,
|
||||
} as Activity;
|
||||
|
||||
lastTrackUrl = lastTrack.url;
|
||||
|
||||
if (currentSettings.showTimestamp) {
|
||||
activity.timestamps = {
|
||||
start: Date.now() / 1000 | 0
|
||||
};
|
||||
}
|
||||
|
||||
if (lastTrack.albumArt) {
|
||||
activity.assets = {
|
||||
large_image: lastTrack.albumArt,
|
||||
large_text: `on ${lastTrack.album}`,
|
||||
}
|
||||
}
|
||||
|
||||
const response = await sendRequest(activity).catch((err) => {
|
||||
verboseLog("--> An error occurred while setting the activity");
|
||||
clearActivity();
|
||||
throw err;
|
||||
});
|
||||
|
||||
verboseLog("--> Successfully set activity!");
|
||||
verboseLog(response);
|
||||
return response;
|
||||
}
|
||||
|
||||
// Stops everything
|
||||
export function flush(): Promise<ResultActivity> {
|
||||
console.log("--> Flushing...");
|
||||
lastActivity = null;
|
||||
updateInterval && clearInterval(updateInterval);
|
||||
|
||||
return clearActivity();
|
||||
}
|
||||
|
||||
// Initializes the plugin
|
||||
export async function initialize() {
|
||||
console.log("--> Initializing...");
|
||||
|
||||
stopped = false;
|
||||
lastActivity = null;
|
||||
lastTrackUrl = null;
|
||||
|
||||
if (updateInterval) clearInterval(updateInterval);
|
||||
if (lastActivity) await clearActivity();
|
||||
|
||||
let tries = 0;
|
||||
|
||||
update().catch((err) => {
|
||||
console.error(err);
|
||||
tries++;
|
||||
});
|
||||
|
||||
|
||||
// Periodically fetches the current scrobble and sets the activity
|
||||
updateInterval = setInterval(
|
||||
() => update()
|
||||
.then(() => tries = 0)
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
|
||||
if (++tries > 3) {
|
||||
console.error("Failed to fetch/set activity 3 times, aborting...");
|
||||
clearInterval(updateInterval);
|
||||
stopped = true;
|
||||
}
|
||||
}),
|
||||
currentSettings.timeInterval
|
||||
);
|
||||
}
|
||||
|
||||
// Discord doesn't allow us to set the activity type to "Listening to" so we have to patch it
|
||||
function patchActivityType() {
|
||||
if (activityTypeUnpatch) {
|
||||
console.warn("This is weird... activityTypeUnpatch is already defined?");
|
||||
activityTypeUnpatch();
|
||||
} else {
|
||||
console.log("Patching activity type...");
|
||||
}
|
||||
|
||||
const nodes = Object.values(FluxDispatcher._actionHandlers._dependencyGraph.nodes);
|
||||
const { actionHandler } = nodes.find((x: any) => x.name === "LocalActivityStore") as any;
|
||||
|
||||
activityTypeUnpatch = patcher.before("LOCAL_ACTIVITY_UPDATE", actionHandler, ([{ activity }]) => {
|
||||
if (activity.name === lastActivity.name) {
|
||||
activity.type = lastActivity.type;
|
||||
}
|
||||
});
|
||||
}
|
||||
export const verboseLog = (...message: any) => currentSettings.verboseLogging && console.log(...message);
|
||||
|
||||
// Plugin entry point
|
||||
export default {
|
||||
onLoad: () => {
|
||||
console.log("Starting last.fm plugin..");
|
||||
patchActivityType();
|
||||
global.activityTypeUnpatch = patchActivityType();
|
||||
global.stopped = false;
|
||||
|
||||
// Options
|
||||
// settings.appName = "Music";
|
||||
// settings.username = "slyde99";
|
||||
// settings.showTimestamp = true;
|
||||
// settings.timeInterval = 5000;
|
||||
// settings.listeningTo = true;
|
||||
// settings.verboseLogging = false;
|
||||
|
||||
// If the user is already logged in (brrr connection), initialize the plugin
|
||||
if (UserStore.getCurrentUser()) {
|
||||
console.log("User is already logged in, initializing...");
|
||||
initialize().catch(console.error);
|
||||
}
|
||||
|
||||
// Just in case the user logs in before the plugin is loaded
|
||||
FluxDispatcher.subscribe("CONNECTION_OPEN", () => {
|
||||
initialize().catch(console.error)
|
||||
initialize().catch(console.error);
|
||||
});
|
||||
},
|
||||
onUnload: () => {
|
||||
console.log("Stopping last.fm...");
|
||||
|
||||
stopped = true;
|
||||
global.stopped = true;
|
||||
|
||||
flush();
|
||||
activityTypeUnpatch?.();
|
||||
activityTypeUnpatch = null;
|
||||
global.activityTypeUnpatch?.();
|
||||
global.activityTypeUnpatch = null;
|
||||
},
|
||||
settings: Settings
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import { NavigationNative, React, ReactNative } from "@vendetta/metro/common";
|
||||
import { storage } from "@vendetta/plugin";
|
||||
import { useProxy } from "@vendetta/storage";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { Forms } from "@vendetta/ui/components";
|
||||
import { currentSettings, flush, initialize } from ".";
|
||||
|
||||
const { ScrollView, TouchableOpacity } = ReactNative;
|
||||
const { FormInput, FormDivider, FormSwitchRow, FormText } = Forms;
|
||||
import { currentSettings } from ".";
|
||||
import { flush, initialize } from "./utils";
|
||||
|
||||
const { ScrollView, TouchableOpacity, Text } = ReactNative;
|
||||
const { FormInput, FormDivider, FormSwitchRow, FormText, FormIcon, FormTextColors } = Forms;
|
||||
|
||||
export default function Settings() {
|
||||
const settings = useProxy(storage) as PluginSettings;
|
||||
|
|
@ -16,9 +19,13 @@ export default function Settings() {
|
|||
headerRight: () => (
|
||||
<TouchableOpacity onPress={() => {
|
||||
for (const key in storage) {
|
||||
currentSettings[key] = settings[key];
|
||||
if (typeof storage[key] === "boolean" || storage[key]) {
|
||||
currentSettings[key] = settings[key];
|
||||
}
|
||||
}
|
||||
flush().then(() => initialize()).then(() => navigation.goBack()).catch(console.error);
|
||||
|
||||
console.log("Applying settings...");
|
||||
flush().then(() => initialize());
|
||||
}}>
|
||||
<FormText style={{ marginRight: 12 }}>UPDATE</FormText>
|
||||
</TouchableOpacity>
|
||||
|
|
@ -29,17 +36,18 @@ export default function Settings() {
|
|||
return (
|
||||
<ScrollView>
|
||||
<FormInput
|
||||
value={settings.appName}
|
||||
value={settings.appName || undefined}
|
||||
onChangeText={(value: string) => settings.appName = value.trim()}
|
||||
title="Application Name"
|
||||
title="Discord Application Name"
|
||||
placeholder="Music"
|
||||
returnKeyType="done"
|
||||
/>
|
||||
<FormDivider />
|
||||
<FormInput
|
||||
value={settings.username}
|
||||
<FormInput required
|
||||
value={settings.username || undefined}
|
||||
onChangeText={(value: string) => settings.username = value.trim()}
|
||||
title="Username"
|
||||
title="Last.fm username"
|
||||
helpText={!settings.username && <Text style={{ color: "#FF0000" }}>{"This field is required!"}</Text>}
|
||||
placeholder="wumpus123"
|
||||
returnKeyType="done"
|
||||
/>
|
||||
|
|
@ -47,6 +55,7 @@ export default function Settings() {
|
|||
<FormSwitchRow
|
||||
label="Show time elapsed"
|
||||
subLabel="Show the time elapsed since the song started playing"
|
||||
leading={<FormIcon source={getAssetIDByName("clock")} />}
|
||||
value={settings.showTimestamp}
|
||||
onValueChange={(value: boolean) => settings.showTimestamp = value}
|
||||
/>
|
||||
|
|
@ -54,6 +63,7 @@ export default function Settings() {
|
|||
<FormSwitchRow
|
||||
label="Set status as listening"
|
||||
subLabel='Set your status as "Listening to" instead of "Playing"'
|
||||
leading={<FormIcon source={getAssetIDByName("ic_headset_neutral")} />}
|
||||
value={settings.listeningTo}
|
||||
onValueChange={(value: boolean) => settings.listeningTo = value}
|
||||
/>
|
||||
|
|
@ -61,6 +71,7 @@ export default function Settings() {
|
|||
<FormSwitchRow
|
||||
label="Verbose logging"
|
||||
subLabel="Log more information to the console for debugging purposes"
|
||||
leading={<FormIcon source={getAssetIDByName("icon-qs-files")} />}
|
||||
value={settings.verboseLogging}
|
||||
onValueChange={(value: boolean) => settings.verboseLogging = value}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -12,3 +12,51 @@ type PluginSettings = {
|
|||
/** Whether or not to show verbose logging */
|
||||
verboseLogging: boolean;
|
||||
};
|
||||
|
||||
// Discord activity, used for SET_ACTIVITY
|
||||
type Activity = {
|
||||
name: string;
|
||||
type: number;
|
||||
details: string;
|
||||
state: string;
|
||||
timestamps?: {
|
||||
start: number;
|
||||
end?: number;
|
||||
};
|
||||
assets?: ActivityAssets;
|
||||
}
|
||||
|
||||
// A type of an object that is returned by the Discord API after setting the activity
|
||||
type ResultActivity = {
|
||||
name: string;
|
||||
type: number;
|
||||
details: string;
|
||||
state: string;
|
||||
timestamps?: {
|
||||
start: number;
|
||||
end?: number;
|
||||
};
|
||||
assets?: ActivityAssets;
|
||||
application_id: string;
|
||||
};
|
||||
|
||||
// The assets of the activity
|
||||
type ActivityAssets = {
|
||||
large_image: string;
|
||||
large_text?: string;
|
||||
} | {
|
||||
small_image: string;
|
||||
small_text?: string;
|
||||
};
|
||||
|
||||
// Last.fm track
|
||||
type Track = {
|
||||
name: string;
|
||||
artist: string;
|
||||
album: string;
|
||||
albumArt: string;
|
||||
url: string;
|
||||
date: string;
|
||||
nowPlaying: boolean;
|
||||
loved: boolean;
|
||||
}
|
||||
|
|
|
|||
205
plugins/Last.fm/src/utils.ts
Normal file
205
plugins/Last.fm/src/utils.ts
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import { patcher } from "@vendetta";
|
||||
import { FluxDispatcher } from "@vendetta/metro/common";
|
||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||
import { showToast } from "@vendetta/ui/toasts";
|
||||
import { currentSettings, global, SET_ACTIVITY, verboseLog } from ".";
|
||||
import Constants from "./constants";
|
||||
|
||||
/** Fetches the latest user's scrobble */
|
||||
async function fetchLatestScrobble(): Promise<Track> {
|
||||
const params = new URLSearchParams({
|
||||
'method': 'user.getrecenttracks',
|
||||
'user': currentSettings.username,
|
||||
'api_key': Constants.LFM_API_KEY,
|
||||
'format': 'json',
|
||||
'limit': '1',
|
||||
'extended': '1'
|
||||
}).toString();
|
||||
|
||||
const info = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`).then(x => x.json());
|
||||
const lastTrack = info.recenttracks?.track?.[0] || Promise.reject(info);
|
||||
return {
|
||||
name: lastTrack.name,
|
||||
artist: lastTrack.artist.name,
|
||||
album: lastTrack.album['#text'],
|
||||
albumArt: handleAlbumCover(lastTrack.image[3]['#text']),
|
||||
url: lastTrack.url,
|
||||
date: lastTrack.date?.['#text'] ?? 'now',
|
||||
nowPlaying: Boolean(lastTrack['@attr']?.nowplaying),
|
||||
loved: lastTrack.loved === '1',
|
||||
} as Track;
|
||||
}
|
||||
|
||||
/** Currently ditches the default album covers
|
||||
* @param cover The album cover given by Last.fm
|
||||
*/
|
||||
function handleAlbumCover(cover: string): string {
|
||||
// If the cover is a default one, return null (empty)
|
||||
if (Constants.DEFAULT_COVER_HASHES.some(x => cover.includes(x))) {
|
||||
return null;
|
||||
}
|
||||
return cover;
|
||||
}
|
||||
|
||||
/** Sends the activity details to Discord */
|
||||
async function sendRequest(activity: Activity): Promise<ResultActivity> {
|
||||
if (global.stopped) {
|
||||
console.log("--> Plugin is unloaded, aborting...");
|
||||
global.updateInterval && clearInterval(global.updateInterval);
|
||||
activity = null;
|
||||
}
|
||||
|
||||
global.lastActivity = activity;
|
||||
|
||||
return await SET_ACTIVITY.handler({
|
||||
isSocketConnected: () => true,
|
||||
socket: {
|
||||
id: 120,
|
||||
application: {
|
||||
id: Constants.APPLICATION_ID,
|
||||
name: activity?.name || "RichPresence"
|
||||
},
|
||||
transport: "ipc"
|
||||
},
|
||||
args: {
|
||||
pid: 120,
|
||||
activity: activity && { ...activity } || null
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Clears the user's activity */
|
||||
function clearActivity(): Promise<ResultActivity> {
|
||||
global.lastActivity && verboseLog("--> Clearing activity...");
|
||||
return sendRequest(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the current scrobble and sets the activity.
|
||||
* This function is actively called by the updateInterval.
|
||||
*/
|
||||
export async function update() {
|
||||
verboseLog("--> Fetching last track...");
|
||||
|
||||
if (!currentSettings.username) {
|
||||
showToast("Last.fm username is not set!", getAssetIDByName("Small"));
|
||||
await flush(); // Flush as we always needs reinitialization
|
||||
throw new Error("Username is not set");
|
||||
}
|
||||
|
||||
const lastTrack = await fetchLatestScrobble().catch(async (err) => {
|
||||
verboseLog("--> An error occurred while fetching the last track, aborting...");
|
||||
await clearActivity().catch();
|
||||
throw err;
|
||||
});
|
||||
|
||||
if (!lastTrack.nowPlaying) {
|
||||
verboseLog("--> Last track is not currently playing, aborting...");
|
||||
return await clearActivity();
|
||||
}
|
||||
|
||||
verboseLog("--> Setting activity...");
|
||||
|
||||
if (global.lastTrackUrl === lastTrack.url) {
|
||||
verboseLog("--> Last track is the same as the previous one, aborting...");
|
||||
return;
|
||||
}
|
||||
|
||||
const activity = {
|
||||
name: currentSettings.appName || "Music",
|
||||
type: currentSettings.listeningTo ? 2 : 0,
|
||||
details: lastTrack.name,
|
||||
state: `by ${lastTrack.artist}`,
|
||||
} as Activity;
|
||||
|
||||
global.lastTrackUrl = lastTrack.url;
|
||||
|
||||
// Set timestamps
|
||||
if (currentSettings.showTimestamp) {
|
||||
activity.timestamps = {
|
||||
start: Date.now() / 1000 | 0
|
||||
};
|
||||
}
|
||||
|
||||
if (lastTrack.albumArt) {
|
||||
activity.assets = {
|
||||
large_image: lastTrack.albumArt,
|
||||
large_text: `on ${lastTrack.album}`,
|
||||
}
|
||||
}
|
||||
|
||||
const response = await sendRequest(activity).catch(async (err) => {
|
||||
verboseLog("--> An error occurred while setting the activity");
|
||||
await clearActivity().catch();
|
||||
throw err;
|
||||
});
|
||||
|
||||
verboseLog("--> Successfully set activity!");
|
||||
verboseLog(response);
|
||||
return response;
|
||||
}
|
||||
|
||||
/** Stops everything */
|
||||
export function flush(): Promise<ResultActivity> {
|
||||
console.log("--> Flushing...");
|
||||
global.lastActivity = null;
|
||||
global.updateInterval && clearInterval(global.updateInterval);
|
||||
|
||||
return clearActivity();
|
||||
}
|
||||
|
||||
/** Initializes the plugin */
|
||||
export async function initialize() {
|
||||
console.log("--> Initializing...");
|
||||
|
||||
if (global.stopped) {
|
||||
throw new Error("Plugin is already stopped!");
|
||||
}
|
||||
|
||||
global.lastTrackUrl = null;
|
||||
|
||||
global.updateInterval && clearInterval(global.updateInterval);
|
||||
global.lastActivity && await clearActivity();
|
||||
|
||||
let tries = 0;
|
||||
|
||||
update().catch((err) => {
|
||||
console.error(err);
|
||||
tries++;
|
||||
});
|
||||
|
||||
// Periodically fetches the current scrobble and sets the activity
|
||||
global.updateInterval = setInterval(
|
||||
() => update()
|
||||
.then(() => tries = 0)
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
|
||||
if (++tries > 3) {
|
||||
console.error("Failed to fetch/set activity 3 times, aborting...");
|
||||
clearInterval(global.updateInterval);
|
||||
global.stopped = true;
|
||||
}
|
||||
}),
|
||||
currentSettings.timeInterval
|
||||
);
|
||||
}
|
||||
|
||||
// Discord doesn't allow us to set the activity type to "Listening to" so we have to patch it
|
||||
export function patchActivityType(): () => boolean {
|
||||
if (global.activityTypeUnpatch) {
|
||||
console.warn("This is weird... activity type override is already defined?");
|
||||
global.activityTypeUnpatch();
|
||||
}
|
||||
|
||||
console.log("Patching activity type...");
|
||||
|
||||
const nodes = Object.values(FluxDispatcher._actionHandlers._dependencyGraph.nodes);
|
||||
const { actionHandler } = nodes.find((x: any) => x.name === "LocalActivityStore") as any;
|
||||
|
||||
return patcher.before("LOCAL_ACTIVITY_UPDATE", actionHandler, ([{ activity }]) => {
|
||||
if (activity && activity.name === global.lastActivity.name) {
|
||||
activity.type = global.lastActivity.type;
|
||||
}
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue