Page MenuHomePhabricator

No OneTemporary

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

Mime Type
text/x-diff
Expires
Mon, Dec 23, 6:14 AM (23 h, 21 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2679356
Default Alt Text
(16 KB)

Event Timeline