Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3508911
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
20 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment