Skip to content

Commit

Permalink
Load global settings from DIM API
Browse files Browse the repository at this point in the history
  • Loading branch information
bhollis committed Jan 14, 2020
1 parent 0182cb4 commit 7c3b747
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 35 deletions.
3 changes: 2 additions & 1 deletion config/content-security-policy.js
Expand Up @@ -44,7 +44,8 @@ module.exports = function csp(env) {
'https://reviews-api.destinytracker.net',
'https://api.tracker.gg',
'https://api.vendorengrams.xyz',
'https://raw.githubusercontent.com'
'https://raw.githubusercontent.com',
'https://api.destinyitemmanager.com'
],
imgSrc: [
SELF,
Expand Down
3 changes: 3 additions & 0 deletions src/Index.tsx
Expand Up @@ -23,6 +23,8 @@ import { saveWishListToIndexedDB } from './app/wishlists/reducer';
import { saveAccountsToIndexedDB } from 'app/accounts/reducer';
import updateCSSVariables from 'app/css-variables';
import { saveVendorDropsToIndexedDB } from 'app/vendorEngramsXyzApi/reducer';
import store from 'app/store/store';
import { loadGlobalSettings } from 'app/dim-api/actions';

polyfill({
holdToDrag: 300,
Expand All @@ -41,6 +43,7 @@ saveWishListToIndexedDB();
saveAccountsToIndexedDB();
saveVendorDropsToIndexedDB();
updateCSSVariables();
store.dispatch(loadGlobalSettings());

// Load some stuff at startup
SyncService.init();
Expand Down
2 changes: 1 addition & 1 deletion src/app/App.tsx
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { UIView } from '@uirouter/react';
import Header from './shell/Header';
import clsx from 'clsx';
import { ActivityTracker } from './dim-ui/ActivityTracker';
import ActivityTracker from './dim-ui/ActivityTracker';
import { connect } from 'react-redux';
import { RootState } from './store/reducers';
import { testFeatureCompatibility } from './compatibility';
Expand Down
19 changes: 19 additions & 0 deletions src/app/dim-api/actions.ts
@@ -0,0 +1,19 @@
import { createAction } from 'typesafe-actions';
import { GlobalSettings, getGlobalSettings } from 'app/dim-api/global-settings';
import { ThunkResult } from 'app/store/reducers';

/** Bulk update global settings after they've been loaded. */
export const settingsLoaded = createAction('dim-api/GLOBAL_SETTINGS_LOADED')<
Partial<GlobalSettings>
>();

export function loadGlobalSettings(): ThunkResult<Promise<void>> {
return async (dispatch, getState) => {
// TODO: better to use a state machine (UNLOADED => LOADING => LOADED)
if (!getState().dimApi.settingsLoaded) {
const globalSettings = await getGlobalSettings();
console.log('globalSettings', globalSettings);
dispatch(settingsLoaded(globalSettings));
}
};
}
25 changes: 25 additions & 0 deletions src/app/dim-api/dim-api-helper.ts
@@ -0,0 +1,25 @@
import { HttpClientConfig } from 'bungie-api-ts/http';
import { stringify } from 'simple-query-string';

/**
* Call one of the unauthenticated DIM APIs.
*/
export async function unauthenticatedApi<T>(config: HttpClientConfig): Promise<T> {
let url = `https://api.destinyitemmanager.com${config.url}`;
if (config.params) {
url = `${url}?${stringify(config.params)}`;
}
const response = await fetch(
new Request(url, {
method: config.method,
body: JSON.stringify(config.body),
headers: {
// TODO: send an API Key
// 'X-API-Key': DIM_API_KEY,
'Content-Type': 'application/json'
}
})
);

return response.json() as Promise<T>;
}
32 changes: 32 additions & 0 deletions src/app/dim-api/global-settings.ts
@@ -0,0 +1,32 @@
import { unauthenticatedApi } from './dim-api-helper';

/**
* Global DIM platform settings from the DIM API.
*/
export interface GlobalSettings {
/** Whether to use the DIM API for */
dimApiEnabled: boolean;
/** Don't allow refresh more often than this many seconds. */
destinyProfileMinimumRefreshInterval: number;
/** Time in seconds to refresh the profile when autoRefresh is true. */
destinyProfileRefreshInterval: number;
/** Whether to refresh profile automatically. */
autoRefresh: boolean;
/** Whether to refresh profile when the page becomes visible after being in the background. */
refreshProfileOnVisible: boolean;
/** Whether to use dirty tricks to bust the Bungie.net cache when users manually refresh. */
bustProfileCacheOnHardRefresh: boolean;
}

export async function getGlobalSettings() {
try {
const response = await unauthenticatedApi<{ settings: GlobalSettings }>({
url: '/platform_info',
method: 'GET'
});
return response.settings;
} catch (e) {
console.log(e);
throw e;
}
}
48 changes: 48 additions & 0 deletions src/app/dim-api/reducer.ts
@@ -0,0 +1,48 @@
import { Reducer } from 'redux';
import * as actions from './actions';
import { ActionType, getType } from 'typesafe-actions';
import _ from 'lodash';
import { GlobalSettings } from 'app/dim-api/global-settings';

export interface DimApiState {
settings: GlobalSettings;
settingsLoaded: boolean;
}

/**
* Global DIM platform settings from the DIM API.
*/
const initialState: DimApiState = {
settingsLoaded: false,
settings: {
dimApiEnabled: true,
destinyProfileMinimumRefreshInterval: 30,
destinyProfileRefreshInterval: 30,
// 2019-12-17 we've been asked to disable auto-refresh
autoRefresh: false,
refreshProfileOnVisible: true,
bustProfileCacheOnHardRefresh: false
}
};

type DimApiAction = ActionType<typeof actions>;

export const dimApi: Reducer<DimApiState, DimApiAction> = (
state: DimApiState = initialState,
action: DimApiAction
) => {
switch (action.type) {
case getType(actions.settingsLoaded):
return {
...state,
settingsLoaded: true,
settings: {
...state.settings,
...action.payload
}
};

default:
return state;
}
};
108 changes: 76 additions & 32 deletions src/app/dim-ui/ActivityTracker.tsx
Expand Up @@ -7,42 +7,64 @@ import { Subscription } from 'rxjs';
import { filter, take } from 'rxjs/operators';
import { dimNeedsUpdate } from 'app/register-service-worker';
import { reloadDIM } from 'app/whats-new/WhatsNewLink';
import { connect } from 'react-redux';
import { RootState } from 'app/store/reducers';

const MIN_REFRESH_INTERVAL = 5 * 1000;
// const AUTO_REFRESH_INTERVAL = 30 * 1000;
interface StoreProps {
/** Don't allow refresh more often than this many seconds. */
destinyProfileMinimumRefreshInterval: number;
/** Time in seconds to refresh the profile when autoRefresh is true. */
destinyProfileRefreshInterval: number;
/** Whether to refresh profile automatically. */
autoRefresh: boolean;
/** Whether to refresh profile when the page becomes visible after being in the background. */
refreshProfileOnVisible: boolean;
}

function mapStateToProps(state: RootState): StoreProps {
const {
destinyProfileMinimumRefreshInterval,
destinyProfileRefreshInterval,
autoRefresh,
refreshProfileOnVisible
} = state.dimApi.settings;

return {
destinyProfileRefreshInterval,
destinyProfileMinimumRefreshInterval,
autoRefresh,
refreshProfileOnVisible
};
}

type Props = StoreProps;

/**
* The activity tracker watches for user activity on the page, and periodically fires
* refresh events if the page is visible and has been interacted with.
*/
export class ActivityTracker extends React.Component {
class ActivityTracker extends React.Component<Props> {
private lastRefreshTimestamp = 0;
private refreshAccountDataInterval?: number;
private refreshSubscription: Subscription;

// Broadcast the refresh signal no more than once per minute
private refresh = _.throttle(
() => {
// Individual pages should listen to this event and decide what to refresh,
// and their services should decide how to cache/dedup refreshes.
// This event should *NOT* be listened to by services!
// TODO: replace this with an observable?
triggerRefresh();
},
MIN_REFRESH_INTERVAL,
{ trailing: false }
);

componentDidMount() {
document.addEventListener('visibilitychange', this.visibilityHandler);
document.addEventListener('online', this.refreshAccountData);

this.startTimer();

// Every time we refresh for any reason, reset the timer
this.refreshSubscription = refresh$.subscribe(() => {
this.clearTimer();
this.startTimer();
});
this.refreshSubscription = refresh$.subscribe(() => this.resetTimer());
}

componentDidUpdate(prevProps: Props) {
if (
prevProps.autoRefresh !== this.props.autoRefresh ||
prevProps.destinyProfileRefreshInterval !== this.props.destinyProfileRefreshInterval
) {
this.resetTimer();
}
}

componentWillUnmount() {
Expand All @@ -56,15 +78,34 @@ export class ActivityTracker extends React.Component {
return null;
}

// tslint:disable-next-line: prefer-function-over-method
private refresh() {
if (
Date.now() - this.lastRefreshTimestamp <
this.props.destinyProfileMinimumRefreshInterval * 1000
) {
return;
}

// Individual pages should listen to this event and decide what to refresh,
// and their services should decide how to cache/dedup refreshes.
// This event should *NOT* be listened to by services!
// TODO: replace this with an observable?
triggerRefresh();
this.lastRefreshTimestamp = Date.now();
}

private resetTimer() {
this.clearTimer();
this.startTimer();
}

private startTimer() {
// 2019-12-17 we've been asked to disable auto-refresh
/*
this.refreshAccountDataInterval = window.setTimeout(
this.refreshAccountData,
AUTO_REFRESH_INTERVAL
);
*/
if (this.props.autoRefresh) {
this.refreshAccountDataInterval = window.setTimeout(
this.refreshAccountData,
this.props.destinyProfileRefreshInterval * 1000
);
}
}

private clearTimer() {
Expand All @@ -73,8 +114,10 @@ export class ActivityTracker extends React.Component {

private visibilityHandler = () => {
if (!document.hidden) {
// 2019-12-17 we've been asked to disable auto-refresh
// this.refreshAccountData();
if (this.props.refreshProfileOnVisible) {
this.refreshAccountData();
} else {
}
} else if (dimNeedsUpdate) {
// Sneaky updates - if DIM is hidden and needs an update, do the update.
reloadDIM();
Expand All @@ -101,8 +144,9 @@ export class ActivityTracker extends React.Component {
.then(this.refreshAccountData);
} else {
// If we didn't refresh because things were disabled, keep the timer going
this.clearTimer();
this.startTimer();
this.resetTimer();
}
};
}

export default connect<StoreProps>(mapStateToProps)(ActivityTracker);
5 changes: 4 additions & 1 deletion src/app/store/reducers.ts
Expand Up @@ -8,6 +8,7 @@ import { LoadoutsState, loadouts } from '../loadout/reducer';
import { WishListsState, wishLists } from '../wishlists/reducer';
import { FarmingState, farming } from '../farming/reducer';
import { ManifestState, manifest } from '../manifest/reducer';
import { DimApiState, dimApi } from '../dim-api/reducer';
import { ThunkAction } from 'redux-thunk';
import { VendorDropsState, vendorDrops } from 'app/vendorEngramsXyzApi/reducer';

Expand All @@ -24,6 +25,7 @@ export interface RootState {
readonly farming: FarmingState;
readonly manifest: ManifestState;
readonly vendorDrops: VendorDropsState;
readonly dimApi: DimApiState;
}

export type ThunkResult<R> = ThunkAction<R, RootState, {}, AnyAction>;
Expand All @@ -38,5 +40,6 @@ export default combineReducers({
wishLists,
farming,
manifest,
vendorDrops
vendorDrops,
dimApi
});

0 comments on commit 7c3b747

Please sign in to comment.