Page MenuHomePhabricator

No OneTemporary

diff --git a/keyserver/src/responders/website-responders.js b/keyserver/src/responders/website-responders.js
index 9c9358e79..709a6521b 100644
--- a/keyserver/src/responders/website-responders.js
+++ b/keyserver/src/responders/website-responders.js
@@ -1,383 +1,384 @@
// @flow
import html from 'common-tags/lib/html';
import type { $Response, $Request } from 'express';
import fs from 'fs';
import _keyBy from 'lodash/fp/keyBy';
import * as React from 'react';
import ReactDOMServer from 'react-dom/server';
import { Provider } from 'react-redux';
import { Route, StaticRouter } from 'react-router';
import { createStore, type Store } from 'redux';
import { promisify } from 'util';
import { daysToEntriesFromEntryInfos } from 'lib/reducers/entry-reducer';
import { freshMessageStore } from 'lib/reducers/message-reducer';
import { mostRecentlyReadThread } from 'lib/selectors/thread-selectors';
import { mostRecentMessageTimestamp } from 'lib/shared/message-utils';
import {
threadHasPermission,
threadIsPending,
parsePendingThreadID,
createPendingThread,
} from 'lib/shared/thread-utils';
import { defaultWebEnabledApps } from 'lib/types/enabled-apps';
import { defaultCalendarFilters } from 'lib/types/filter-types';
import { defaultNumberPerThread } from 'lib/types/message-types';
import { defaultEnabledReports } from 'lib/types/report-types';
import { defaultConnectionInfo } from 'lib/types/socket-types';
import { threadPermissions, threadTypes } from 'lib/types/thread-types';
import type { CurrentUserInfo } from 'lib/types/user-types';
import { currentDateInTimeZone } from 'lib/utils/date-utils';
import { ServerError } from 'lib/utils/errors';
import { promiseAll } from 'lib/utils/promises';
import { reducer } from 'web/redux/redux-setup';
import type { AppState, Action } from 'web/redux/redux-setup';
import getTitle from 'web/title/getTitle';
import { navInfoFromURL } from 'web/url-utils';
import { fetchEntryInfos } from '../fetchers/entry-fetchers';
import { fetchMessageInfos } from '../fetchers/message-fetchers';
import { fetchThreadInfos } from '../fetchers/thread-fetchers';
import {
fetchCurrentUserInfo,
fetchKnownUserInfos,
} from '../fetchers/user-fetchers';
import { setNewSession } from '../session/cookies';
import { Viewer } from '../session/viewer';
import { streamJSON, waitForStream } from '../utils/json-stream';
import {
getAppURLFactsFromRequestURL,
clientPathFromRouterPath,
} from '../utils/urls';
const { renderToNodeStream } = ReactDOMServer;
const access = promisify(fs.access);
const googleFontsURL =
'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=Inter:wght@400;500;600&display=swap';
const localFontsURL = 'fonts/local-fonts.css';
async function getFontsURL() {
try {
await access(localFontsURL);
return localFontsURL;
} catch {
return googleFontsURL;
}
}
type AssetInfo = { jsURL: string, fontsURL: string, cssInclude: string };
let assetInfo: ?AssetInfo = null;
async function getAssetInfo() {
if (assetInfo) {
return assetInfo;
}
if (process.env.NODE_ENV === 'development') {
const fontsURL = await getFontsURL();
assetInfo = {
jsURL: 'http://localhost:8080/dev.build.js',
fontsURL,
cssInclude: '',
};
return assetInfo;
}
// $FlowFixMe web/dist doesn't always exist
const { default: assets } = await import('web/dist/assets');
assetInfo = {
jsURL: `compiled/${assets.browser.js}`,
fontsURL: googleFontsURL,
cssInclude: html`
<link
rel="stylesheet"
type="text/css"
href="compiled/${assets.browser.css}"
/>
`,
};
return assetInfo;
}
let webpackCompiledRootComponent: ?React.ComponentType<{}> = null;
async function getWebpackCompiledRootComponentForSSR() {
if (webpackCompiledRootComponent) {
return webpackCompiledRootComponent;
}
try {
// $FlowFixMe web/dist doesn't always exist
const webpackBuild = await import('web/dist/app.build.cjs');
webpackCompiledRootComponent = webpackBuild.default.default;
return webpackCompiledRootComponent;
} catch {
throw new Error(
'Could not load app.build.cjs. ' +
'Did you forget to run `yarn dev` in the web folder?',
);
}
}
async function websiteResponder(
viewer: Viewer,
req: $Request,
res: $Response,
): Promise<void> {
const appURLFacts = getAppURLFactsFromRequestURL(req.originalUrl);
const { basePath, baseDomain } = appURLFacts;
const baseURL = basePath.replace(/\/$/, '');
const baseHref = baseDomain + baseURL;
const appPromise = getWebpackCompiledRootComponentForSSR();
let initialNavInfo;
try {
initialNavInfo = navInfoFromURL(req.url, {
now: currentDateInTimeZone(viewer.timeZone),
});
} catch (e) {
throw new ServerError(e.message);
}
const calendarQuery = {
startDate: initialNavInfo.startDate,
endDate: initialNavInfo.endDate,
filters: defaultCalendarFilters,
};
const messageSelectionCriteria = { joinedThreads: true };
const initialTime = Date.now();
const assetInfoPromise = getAssetInfo();
const threadInfoPromise = fetchThreadInfos(viewer);
const messageInfoPromise = fetchMessageInfos(
viewer,
messageSelectionCriteria,
defaultNumberPerThread,
);
const entryInfoPromise = fetchEntryInfos(viewer, [calendarQuery]);
const currentUserInfoPromise = fetchCurrentUserInfo(viewer);
const userInfoPromise = fetchKnownUserInfos(viewer);
const sessionIDPromise = (async () => {
if (viewer.loggedIn) {
await setNewSession(viewer, calendarQuery, initialTime);
}
return viewer.sessionID;
})();
const threadStorePromise = (async () => {
const { threadInfos } = await threadInfoPromise;
return { threadInfos };
})();
const messageStorePromise = (async () => {
const [
{ threadInfos },
{ rawMessageInfos, truncationStatuses },
] = await Promise.all([threadInfoPromise, messageInfoPromise]);
const { messageStore: freshStore } = freshMessageStore(
rawMessageInfos,
truncationStatuses,
mostRecentMessageTimestamp(rawMessageInfos, initialTime),
threadInfos,
);
return freshStore;
})();
const entryStorePromise = (async () => {
const { rawEntryInfos } = await entryInfoPromise;
return {
entryInfos: _keyBy('id')(rawEntryInfos),
daysToEntries: daysToEntriesFromEntryInfos(rawEntryInfos),
lastUserInteractionCalendar: initialTime,
};
})();
const userStorePromise = (async () => {
const userInfos = await userInfoPromise;
return { userInfos, inconsistencyReports: [] };
})();
const navInfoPromise = (async () => {
const [
{ threadInfos },
messageStore,
currentUserInfo,
userStore,
] = await Promise.all([
threadInfoPromise,
messageStorePromise,
currentUserInfoPromise,
userStorePromise,
]);
const finalNavInfo = initialNavInfo;
const requestedActiveChatThreadID = finalNavInfo.activeChatThreadID;
if (
requestedActiveChatThreadID &&
!threadIsPending(requestedActiveChatThreadID) &&
!threadHasPermission(
threadInfos[requestedActiveChatThreadID],
threadPermissions.VISIBLE,
)
) {
finalNavInfo.activeChatThreadID = null;
}
if (!finalNavInfo.activeChatThreadID) {
const mostRecentThread = mostRecentlyReadThread(
messageStore,
threadInfos,
);
if (mostRecentThread) {
finalNavInfo.activeChatThreadID = mostRecentThread;
}
}
if (
finalNavInfo.activeChatThreadID &&
threadIsPending(finalNavInfo.activeChatThreadID) &&
finalNavInfo.pendingThread?.id !== finalNavInfo.activeChatThreadID
) {
const pendingThreadData = parsePendingThreadID(
finalNavInfo.activeChatThreadID,
);
if (
pendingThreadData &&
pendingThreadData.threadType !== threadTypes.SIDEBAR &&
currentUserInfo.id
) {
const { userInfos } = userStore;
const members = pendingThreadData.memberIDs
.map(id => userInfos[id])
.filter(Boolean);
const newPendingThread = createPendingThread({
viewerID: currentUserInfo.id,
threadType: pendingThreadData.threadType,
members,
});
finalNavInfo.activeChatThreadID = newPendingThread.id;
finalNavInfo.pendingThread = newPendingThread;
}
}
return finalNavInfo;
})();
const { jsURL, fontsURL, cssInclude } = await assetInfoPromise;
// prettier-ignore
res.write(html`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>${getTitle(0)}</title>
<base href="${basePath}" />
<link rel="stylesheet" type="text/css" href="${fontsURL}" />
${cssInclude}
<link
rel="apple-touch-icon"
sizes="180x180"
href="apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="favicon-16x16.png"
/>
<link rel="manifest" href="site.webmanifest" />
<meta name="apple-mobile-web-app-title" content="Comm" />
<meta name="application-name" content="Comm" />
<meta name="msapplication-TileColor" content="#b91d47" />
<meta name="theme-color" content="#b91d47" />
</head>
<body>
<div id="react-root">
`);
const statePromises = {
navInfo: navInfoPromise,
currentUserInfo: ((currentUserInfoPromise: any): Promise<CurrentUserInfo>),
sessionID: sessionIDPromise,
entryStore: entryStorePromise,
threadStore: threadStorePromise,
userStore: userStorePromise,
messageStore: messageStorePromise,
updatesCurrentAsOf: initialTime,
loadingStatuses: {},
calendarFilters: defaultCalendarFilters,
// We can use paths local to the <base href> on web
urlPrefix: '',
windowDimensions: { width: 0, height: 0 },
baseHref,
connection: {
...defaultConnectionInfo('web', viewer.timeZone),
actualizedCalendarQuery: calendarQuery,
},
watchedThreadIDs: [],
lifecycleState: 'active',
enabledApps: defaultWebEnabledApps,
reportStore: {
enabledReports: defaultEnabledReports,
queuedReports: [],
},
nextLocalID: 0,
timeZone: viewer.timeZone,
userAgent: viewer.userAgent,
cookie: undefined,
deviceToken: undefined,
dataLoaded: viewer.loggedIn,
windowActive: true,
+ _persist: null,
};
const [stateResult, App] = await Promise.all([
promiseAll(statePromises),
appPromise,
]);
const state: AppState = { ...stateResult };
const store: Store<AppState, Action> = createStore(reducer, state);
const routerContext = {};
const clientPath = clientPathFromRouterPath(req.url, appURLFacts);
const reactStream = renderToNodeStream(
<Provider store={store}>
<StaticRouter
location={clientPath}
basename={baseURL}
context={routerContext}
>
<Route path="*" component={App} />
</StaticRouter>
</Provider>,
);
if (routerContext.url) {
throw new ServerError('URL modified during server render!');
}
reactStream.pipe(res, { end: false });
await waitForStream(reactStream);
res.write(html`
</div>
<script>
var preloadedState =
`);
const filteredStatePromises = {
...statePromises,
timeZone: null,
userAgent: null,
};
const jsonStream = streamJSON(res, filteredStatePromises);
await waitForStream(jsonStream);
res.end(html`
;
var baseURL = "${baseURL}";
</script>
<script src="${jsURL}"></script>
</body>
</html>
`);
}
export { websiteResponder };
diff --git a/web/redux/redux-setup.js b/web/redux/redux-setup.js
index 780ab2835..e2350407b 100644
--- a/web/redux/redux-setup.js
+++ b/web/redux/redux-setup.js
@@ -1,232 +1,234 @@
// @flow
import invariant from 'invariant';
+import type { PersistState } from 'redux-persist/src/types';
import {
logOutActionTypes,
deleteAccountActionTypes,
} from 'lib/actions/user-actions';
import baseReducer from 'lib/reducers/master-reducer';
import { mostRecentlyReadThreadSelector } from 'lib/selectors/thread-selectors';
import { isLoggedIn } from 'lib/selectors/user-selectors';
import { invalidSessionDowngrade } from 'lib/shared/account-utils';
import type { Shape } from 'lib/types/core';
import type { EnabledApps } from 'lib/types/enabled-apps';
import type { EntryStore } from 'lib/types/entry-types';
import type { CalendarFilter } from 'lib/types/filter-types';
import type { LifecycleState } from 'lib/types/lifecycle-state-types';
import type { LoadingStatus } from 'lib/types/loading-types';
import type { MessageStore } from 'lib/types/message-types';
import type { BaseAction } from 'lib/types/redux-types';
import type { ReportStore } from 'lib/types/report-types';
import type { ConnectionInfo } from 'lib/types/socket-types';
import type { ThreadStore } from 'lib/types/thread-types';
import type { CurrentUserInfo, UserStore } from 'lib/types/user-types';
import { setNewSessionActionType } from 'lib/utils/action-utils';
import { activeThreadSelector } from '../selectors/nav-selectors';
import { type NavInfo, updateNavInfoActionType } from '../types/nav-types';
import { updateWindowActiveActionType } from './action-types';
import reduceNavInfo from './nav-reducer';
import { getVisibility } from './visibility';
export type WindowDimensions = { width: number, height: number };
export type AppState = {
navInfo: NavInfo,
currentUserInfo: ?CurrentUserInfo,
sessionID: ?string,
entryStore: EntryStore,
threadStore: ThreadStore,
userStore: UserStore,
messageStore: MessageStore,
updatesCurrentAsOf: number,
loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } },
calendarFilters: $ReadOnlyArray<CalendarFilter>,
urlPrefix: string,
windowDimensions: WindowDimensions,
cookie?: void,
deviceToken?: void,
baseHref: string,
connection: ConnectionInfo,
watchedThreadIDs: $ReadOnlyArray<string>,
lifecycleState: LifecycleState,
enabledApps: EnabledApps,
reportStore: ReportStore,
nextLocalID: number,
timeZone: ?string,
userAgent: ?string,
dataLoaded: boolean,
windowActive: boolean,
+ _persist: ?PersistState,
};
export const updateWindowDimensions = 'UPDATE_WINDOW_DIMENSIONS';
export type Action =
| BaseAction
| { type: 'UPDATE_NAV_INFO', payload: Shape<NavInfo> }
| {
type: 'UPDATE_WINDOW_DIMENSIONS',
payload: WindowDimensions,
}
| {
type: 'UPDATE_WINDOW_ACTIVE',
payload: boolean,
};
export function reducer(oldState: AppState | void, action: Action): AppState {
invariant(oldState, 'should be set');
let state = oldState;
if (action.type === updateWindowDimensions) {
return validateState(oldState, {
...state,
windowDimensions: action.payload,
});
} else if (action.type === updateWindowActiveActionType) {
return validateState(oldState, {
...state,
windowActive: action.payload,
});
} else if (action.type === setNewSessionActionType) {
if (
invalidSessionDowngrade(
oldState,
action.payload.sessionChange.currentUserInfo,
action.payload.preRequestUserState,
)
) {
return oldState;
}
state = {
...state,
sessionID: action.payload.sessionChange.sessionID,
};
} else if (
(action.type === logOutActionTypes.success &&
invalidSessionDowngrade(
oldState,
action.payload.currentUserInfo,
action.payload.preRequestUserState,
)) ||
(action.type === deleteAccountActionTypes.success &&
invalidSessionDowngrade(
oldState,
action.payload.currentUserInfo,
action.payload.preRequestUserState,
))
) {
return oldState;
}
if (action.type !== updateNavInfoActionType) {
state = baseReducer(state, action).state;
}
state = {
...state,
navInfo: reduceNavInfo(
state.navInfo,
action,
state.threadStore.threadInfos,
),
};
return validateState(oldState, state);
}
function validateState(oldState: AppState, state: AppState): AppState {
if (
(state.navInfo.activeChatThreadID &&
!state.navInfo.pendingThread &&
!state.threadStore.threadInfos[state.navInfo.activeChatThreadID]) ||
(!state.navInfo.activeChatThreadID && isLoggedIn(state))
) {
// Makes sure the active thread always exists
state = {
...state,
navInfo: {
...state.navInfo,
activeChatThreadID: mostRecentlyReadThreadSelector(state),
},
};
}
const activeThread = activeThreadSelector(state);
if (
activeThread &&
!state.navInfo.pendingThread &&
state.threadStore.threadInfos[activeThread].currentUser.unread &&
getVisibility().hidden()
) {
console.warn(
`thread ${activeThread} is active and unread, ` +
'but visibilityjs reports the window is not visible',
);
}
if (
activeThread &&
!state.navInfo.pendingThread &&
state.threadStore.threadInfos[activeThread].currentUser.unread &&
typeof document !== 'undefined' &&
document &&
'hasFocus' in document &&
!document.hasFocus()
) {
console.warn(
`thread ${activeThread} is active and unread, ` +
'but document.hasFocus() is false',
);
}
if (
activeThread &&
!getVisibility().hidden() &&
typeof document !== 'undefined' &&
document &&
'hasFocus' in document &&
document.hasFocus() &&
!state.navInfo.pendingThread &&
state.threadStore.threadInfos[activeThread].currentUser.unread
) {
// Makes sure a currently focused thread is never unread
state = {
...state,
threadStore: {
...state.threadStore,
threadInfos: {
...state.threadStore.threadInfos,
[activeThread]: {
...state.threadStore.threadInfos[activeThread],
currentUser: {
...state.threadStore.threadInfos[activeThread].currentUser,
unread: false,
},
},
},
},
};
}
const oldActiveThread = activeThreadSelector(oldState);
if (
activeThread &&
oldActiveThread !== activeThread &&
state.messageStore.threads[activeThread]
) {
// Update messageStore.threads[activeThread].lastNavigatedTo
state = {
...state,
messageStore: {
...state.messageStore,
threads: {
...state.messageStore.threads,
[activeThread]: {
...state.messageStore.threads[activeThread],
lastNavigatedTo: Date.now(),
},
},
},
};
}
return state;
}
diff --git a/web/root.js b/web/root.js
index 75cfd2c0c..7cfab234a 100644
--- a/web/root.js
+++ b/web/root.js
@@ -1,28 +1,42 @@
// @flow
import * as React from 'react';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware, type Store } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
+import { persistReducer, persistStore } from 'redux-persist';
+import { PersistGate } from 'redux-persist/integration/react';
+import storage from 'redux-persist/lib/storage';
import thunk from 'redux-thunk';
import { reduxLoggerMiddleware } from 'lib/utils/action-logger';
import HotRoot from './hot';
import { reducer } from './redux/redux-setup';
import type { AppState, Action } from './redux/redux-setup';
+const persistConfig = {
+ key: 'root',
+ storage,
+ whitelist: ['enabledApps'],
+ version: 0,
+};
+
declare var preloadedState: AppState;
+const persistedReducer = persistReducer(persistConfig, reducer);
const store: Store<AppState, Action> = createStore(
- reducer,
+ persistedReducer,
preloadedState,
composeWithDevTools({})(applyMiddleware(thunk, reduxLoggerMiddleware)),
);
+const persistor = persistStore(store);
const RootProvider = (): React.Node => (
<Provider store={store}>
- <HotRoot />
+ <PersistGate persistor={persistor}>
+ <HotRoot />
+ </PersistGate>
</Provider>
);
export default RootProvider;

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 23, 12:21 AM (3 h, 4 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2689996
Default Alt Text
(20 KB)

Event Timeline