Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3509559
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
16 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/desktop/main.cjs b/desktop/main.cjs
index aad5fd564..b9d03ea69 100644
--- a/desktop/main.cjs
+++ b/desktop/main.cjs
@@ -1,234 +1,238 @@
const {
app,
BrowserWindow,
shell,
Menu,
ipcMain,
systemPreferences,
} = require('electron');
const fs = require('fs');
const path = require('path');
const isDev = process.env.ENV === 'dev';
const url = isDev ? 'http://localhost/comm/' : 'https://web.comm.app';
const isMac = process.platform === 'darwin';
const scrollbarCSS = fs.promises.readFile(
path.join(__dirname, 'scrollbar.css'),
'utf8',
);
const setApplicationMenu = () => {
let mainMenu = [];
if (isMac) {
mainMenu = [
{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' },
],
},
];
}
const viewMenu = {
label: 'View',
submenu: [
{ role: 'reload' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' },
{ role: 'toggleDevTools' },
],
};
const windowMenu = {
label: 'Window',
submenu: [
{ role: 'minimize' },
...(isMac
? [
{ type: 'separator' },
{ role: 'front' },
{ type: 'separator' },
{ role: 'window' },
]
: [{ role: 'close' }]),
],
};
const menu = Menu.buildFromTemplate([
...mainMenu,
{ role: 'fileMenu' },
{ role: 'editMenu' },
viewMenu,
windowMenu,
]);
Menu.setApplicationMenu(menu);
};
const createMainWindow = () => {
const win = new BrowserWindow({
show: false,
width: 1300,
height: 800,
minWidth: 1100,
minHeight: 600,
titleBarStyle: 'hidden',
trafficLightPosition: { x: 20, y: 24 },
backgroundColor: '#0A0A0A',
webPreferences: {
preload: path.join(__dirname, 'preload.cjs'),
},
});
const updateNavigationState = () => {
win.webContents.send('on-navigate', {
canGoBack: win.webContents.canGoBack(),
canGoForward: win.webContents.canGoForward(),
});
};
win.webContents.on('did-navigate-in-page', updateNavigationState);
ipcMain.on('clear-history', () => {
win.webContents.clearHistory();
updateNavigationState();
});
ipcMain.on('double-click-top-bar', () => {
if (isMac) {
// Possible values for AppleActionOnDoubleClick are Maximize,
// Minimize or None. We handle the last two inside this if.
// Maximize (which is the only behaviour for other platforms)
// is handled in the later block.
const action = systemPreferences.getUserDefault(
'AppleActionOnDoubleClick',
'string',
);
if (action === 'None') {
return;
} else if (action === 'Minimize') {
win.minimize();
return;
}
}
if (win.isMaximized()) {
win.unmaximize();
} else {
win.maximize();
}
});
+ ipcMain.on('set-badge', (event, value) => {
+ app.dock.setBadge(value?.toString() ?? '');
+ });
+
win.webContents.setWindowOpenHandler(({ url: openURL }) => {
shell.openExternal(openURL);
// Returning 'deny' prevents a new electron window from being created
return { action: 'deny' };
});
(async () => {
const css = await scrollbarCSS;
win.webContents.insertCSS(css);
})();
win.loadURL(url);
return win;
};
const createSplashWindow = () => {
const win = new BrowserWindow({
width: 300,
height: 300,
resizable: false,
frame: false,
alwaysOnTop: true,
center: true,
backgroundColor: '#111827',
});
win.loadFile(path.join(__dirname, 'pages', 'splash.html'));
return win;
};
const createErrorWindow = () => {
const win = new BrowserWindow({
show: false,
width: 400,
height: 300,
resizable: false,
center: true,
titleBarStyle: 'hidden',
trafficLightPosition: { x: 20, y: 24 },
backgroundColor: '#111827',
});
win.on('close', () => {
app.quit();
});
win.loadFile(path.join(__dirname, 'pages', 'error.html'));
return win;
};
const show = () => {
const splash = createSplashWindow();
const error = createErrorWindow();
const main = createMainWindow();
let loadedSuccessfully = true;
main.webContents.on('did-fail-load', () => {
loadedSuccessfully = false;
if (!splash.isDestroyed()) {
splash.destroy();
}
if (!error.isDestroyed()) {
error.show();
}
setTimeout(() => {
loadedSuccessfully = true;
main.loadURL(url);
}, 1000);
});
main.webContents.on('did-finish-load', () => {
if (loadedSuccessfully) {
if (!splash.isDestroyed()) {
splash.destroy();
}
if (!error.isDestroyed()) {
error.destroy();
}
main.show();
}
});
};
app.setName('Comm');
setApplicationMenu();
(async () => {
await app.whenReady();
show();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
show();
}
});
})();
app.on('window-all-closed', () => {
if (!isMac) {
app.quit();
}
});
diff --git a/desktop/preload.cjs b/desktop/preload.cjs
index d5052680b..319a91f1e 100644
--- a/desktop/preload.cjs
+++ b/desktop/preload.cjs
@@ -1,13 +1,14 @@
const { contextBridge, ipcRenderer } = require('electron');
const bridge = {
onNavigate: callback => {
const withEvent = (event, ...args) => callback(...args);
ipcRenderer.on('on-navigate', withEvent);
return () => ipcRenderer.removeListener('on-navigate', withEvent);
},
clearHistory: () => ipcRenderer.send('clear-history'),
doubleClickTopBar: () => ipcRenderer.send('double-click-top-bar'),
+ setBadge: value => ipcRenderer.send('set-badge', value),
};
contextBridge.exposeInMainWorld('electronContextBridge', bridge);
diff --git a/web/app.react.js b/web/app.react.js
index 152666b7a..cbbc1c4a5 100644
--- a/web/app.react.js
+++ b/web/app.react.js
@@ -1,311 +1,312 @@
// @flow
import 'basscss/css/basscss.min.css';
import './theme.css';
import { config as faConfig } from '@fortawesome/fontawesome-svg-core';
import classnames from 'classnames';
import _isEqual from 'lodash/fp/isEqual';
import * as React from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useDispatch } from 'react-redux';
import {
fetchEntriesActionTypes,
updateCalendarQueryActionTypes,
} from 'lib/actions/entry-actions';
import {
ModalProvider,
useModalContext,
} from 'lib/components/modal-provider.react';
import {
createLoadingStatusSelector,
combineLoadingStatuses,
} from 'lib/selectors/loading-selectors';
import { unreadCount } from 'lib/selectors/thread-selectors';
import { isLoggedIn } from 'lib/selectors/user-selectors';
import type { LoadingStatus } from 'lib/types/loading-types';
import type { Dispatch } from 'lib/types/redux-types';
import { registerConfig } from 'lib/utils/config';
import AppsDirectory from './apps/apps-directory.react';
import Calendar from './calendar/calendar.react';
import Chat from './chat/chat.react';
import { TooltipProvider } from './chat/tooltip-provider';
import NavigationArrows from './components/navigation-arrows.react';
import electron from './electron';
import InputStateContainer from './input/input-state-container.react';
import LoadingIndicator from './loading-indicator.react';
import { MenuProvider } from './menu-provider.react';
import { updateNavInfoActionType } from './redux/action-types';
import DeviceIDUpdater from './redux/device-id-updater';
import DisconnectedBar from './redux/disconnected-bar';
import DisconnectedBarVisibilityHandler from './redux/disconnected-bar-visibility-handler';
import FocusHandler from './redux/focus-handler.react';
import { useSelector } from './redux/redux-utils';
import VisibilityHandler from './redux/visibility-handler.react';
import history from './router-history';
import AccountSettings from './settings/account-settings.react';
import DangerZone from './settings/danger-zone.react';
import LeftLayoutAside from './sidebar/left-layout-aside.react';
import Splash from './splash/splash.react';
import './typography.css';
import css from './style.css';
import getTitle from './title/getTitle';
import { type NavInfo } from './types/nav-types';
import { canonicalURLFromReduxState, navInfoFromURL } from './url-utils';
// We want Webpack's css-loader and style-loader to handle the Fontawesome CSS,
// so we disable the autoAddCss logic and import the CSS file. Otherwise every
// icon flashes huge for a second before the CSS is loaded.
import '@fortawesome/fontawesome-svg-core/styles.css';
faConfig.autoAddCss = false;
registerConfig({
// We can't securely cache credentials on web, so we have no way to recover
// from a cookie invalidation
resolveInvalidatedCookie: null,
// We use httponly cookies on web to protect against XSS attacks, so we have
// no access to the cookies from JavaScript
setCookieOnRequest: false,
setSessionIDOnRequest: true,
// Never reset the calendar range
calendarRangeInactivityLimit: null,
platformDetails: { platform: 'web' },
});
type BaseProps = {
+location: {
+pathname: string,
...
},
};
type Props = {
...BaseProps,
// Redux state
+navInfo: NavInfo,
+entriesLoadingStatus: LoadingStatus,
+loggedIn: boolean,
+activeThreadCurrentlyUnread: boolean,
// Redux dispatch functions
+dispatch: Dispatch,
+modals: $ReadOnlyArray<React.Node>,
};
class App extends React.PureComponent<Props> {
componentDidMount() {
const {
navInfo,
location: { pathname },
loggedIn,
} = this.props;
const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn);
if (pathname !== newURL) {
history.replace(newURL);
}
}
componentDidUpdate(prevProps: Props) {
const {
navInfo,
location: { pathname },
loggedIn,
} = this.props;
if (!_isEqual(navInfo)(prevProps.navInfo)) {
const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn);
if (newURL !== pathname) {
history.push(newURL);
}
} else if (pathname !== prevProps.location.pathname) {
const newNavInfo = navInfoFromURL(pathname, { navInfo });
if (!_isEqual(newNavInfo)(navInfo)) {
this.props.dispatch({
type: updateNavInfoActionType,
payload: newNavInfo,
});
}
} else if (loggedIn !== prevProps.loggedIn) {
const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn);
if (newURL !== pathname) {
history.replace(newURL);
}
}
if (loggedIn !== prevProps.loggedIn) {
electron?.clearHistory();
}
}
onWordmarkClicked = () => {
this.props.dispatch({
type: updateNavInfoActionType,
payload: { tab: 'chat' },
});
};
render() {
let content;
if (this.props.loggedIn) {
content = this.renderMainContent();
} else {
content = <Splash />;
}
return (
<DndProvider backend={HTML5Backend}>
<TooltipProvider>
<MenuProvider>
<FocusHandler />
<VisibilityHandler />
<DeviceIDUpdater />
{content}
{this.props.modals}
</MenuProvider>
</TooltipProvider>
</DndProvider>
);
}
onHeaderDoubleClick = () => electron?.doubleClickTopBar();
stopDoubleClickPropagation = electron ? e => e.stopPropagation() : null;
renderMainContent() {
let mainContent;
const { tab, settingsSection } = this.props.navInfo;
if (tab === 'calendar') {
mainContent = <Calendar url={this.props.location.pathname} />;
} else if (tab === 'chat') {
mainContent = <Chat />;
} else if (tab === 'apps') {
mainContent = <AppsDirectory />;
} else if (tab === 'settings') {
if (settingsSection === 'account') {
mainContent = <AccountSettings />;
} else if (settingsSection === 'danger-zone') {
mainContent = <DangerZone />;
}
}
let navigationArrows = null;
if (electron) {
navigationArrows = <NavigationArrows />;
}
const headerClasses = classnames({
[css.header]: true,
[css['electron-draggable']]: electron,
});
const wordmarkClasses = classnames({
[css.wordmark]: true,
[css['electron-non-draggable']]: electron,
});
return (
<div className={css.layout}>
<DisconnectedBarVisibilityHandler />
<DisconnectedBar />
<header
className={headerClasses}
onDoubleClick={this.onHeaderDoubleClick}
>
<div className={css['main-header']}>
<h1 className={wordmarkClasses}>
<a
title="Comm Home"
aria-label="Go to Comm Home"
onClick={this.onWordmarkClicked}
onDoubleClick={this.stopDoubleClickPropagation}
>
Comm
</a>
</h1>
{navigationArrows}
<div className={css['upper-right']}>
<LoadingIndicator
status={this.props.entriesLoadingStatus}
size="medium"
loadingClassName={css['page-loading']}
errorClassName={css['page-error']}
/>
</div>
</div>
</header>
<InputStateContainer>
<div className={css['main-content-container']}>
<div className={css['main-content']}>{mainContent}</div>
</div>
</InputStateContainer>
<LeftLayoutAside />
</div>
);
}
}
const fetchEntriesLoadingStatusSelector = createLoadingStatusSelector(
fetchEntriesActionTypes,
);
const updateCalendarQueryLoadingStatusSelector = createLoadingStatusSelector(
updateCalendarQueryActionTypes,
);
const ConnectedApp: React.ComponentType<BaseProps> = React.memo<BaseProps>(
function ConnectedApp(props) {
const activeChatThreadID = useSelector(
state => state.navInfo.activeChatThreadID,
);
const navInfo = useSelector(state => state.navInfo);
const fetchEntriesLoadingStatus = useSelector(
fetchEntriesLoadingStatusSelector,
);
const updateCalendarQueryLoadingStatus = useSelector(
updateCalendarQueryLoadingStatusSelector,
);
const entriesLoadingStatus = combineLoadingStatuses(
fetchEntriesLoadingStatus,
updateCalendarQueryLoadingStatus,
);
const loggedIn = useSelector(isLoggedIn);
const activeThreadCurrentlyUnread = useSelector(
state =>
!activeChatThreadID ||
!!state.threadStore.threadInfos[activeChatThreadID]?.currentUser.unread,
);
const boundUnreadCount = useSelector(unreadCount);
React.useEffect(() => {
document.title = getTitle(boundUnreadCount);
+ electron?.setBadge(boundUnreadCount === 0 ? null : boundUnreadCount);
}, [boundUnreadCount]);
const dispatch = useDispatch();
const modalContext = useModalContext();
const modals = React.useMemo(
() =>
modalContext.modals.map(([modal, key]) => (
<React.Fragment key={key}>{modal}</React.Fragment>
)),
[modalContext.modals],
);
return (
<App
{...props}
navInfo={navInfo}
entriesLoadingStatus={entriesLoadingStatus}
loggedIn={loggedIn}
activeThreadCurrentlyUnread={activeThreadCurrentlyUnread}
dispatch={dispatch}
modals={modals}
/>
);
},
);
function AppWithProvider(props: BaseProps): React.Node {
return (
<ModalProvider>
<ConnectedApp {...props} />
</ModalProvider>
);
}
export default AppWithProvider;
diff --git a/web/electron.js b/web/electron.js
index ff45ba625..90b758321 100644
--- a/web/electron.js
+++ b/web/electron.js
@@ -1,17 +1,18 @@
// @flow
declare var electronContextBridge;
type OnNavigateListener = ({
+canGoBack: boolean,
+canGoForward: boolean,
}) => void;
const electron: null | {
// Returns a callback that you can call to remove the listener
+onNavigate: OnNavigateListener => () => void,
+clearHistory: () => void,
+doubleClickTopBar: () => void,
+ +setBadge: (value: string | number | null) => void,
} = typeof electronContextBridge === 'undefined' ? null : electronContextBridge;
export default electron;
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 6:14 AM (17 h, 37 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2679356
Default Alt Text
(16 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment