Page MenuHomePhabricator

No OneTemporary

diff --git a/web/app.react.js b/web/app.react.js
index 2c30ee608..8656a6bb2 100644
--- a/web/app.react.js
+++ b/web/app.react.js
@@ -1,348 +1,348 @@
// @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.js';
import * as React from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useDispatch } from 'react-redux';
import { WagmiConfig } from 'wagmi';
import {
fetchEntriesActionTypes,
updateCalendarQueryActionTypes,
} from 'lib/actions/entry-actions.js';
import {
ModalProvider,
useModalContext,
} from 'lib/components/modal-provider.react.js';
import {
createLoadingStatusSelector,
combineLoadingStatuses,
} from 'lib/selectors/loading-selectors.js';
import { unreadCount } from 'lib/selectors/thread-selectors.js';
import { isLoggedIn } from 'lib/selectors/user-selectors.js';
import type { LoadingStatus } from 'lib/types/loading-types.js';
import type { Dispatch } from 'lib/types/redux-types.js';
import { registerConfig } from 'lib/utils/config.js';
import { WagmiENSCacheProvider, wagmiClient } from 'lib/utils/wagmi-utils.js';
import Calendar from './calendar/calendar.react.js';
import Chat from './chat/chat.react.js';
import { TooltipProvider } from './chat/tooltip-provider.js';
import NavigationArrows from './components/navigation-arrows.react.js';
import electron from './electron.js';
import InputStateContainer from './input/input-state-container.react.js';
import LoadingIndicator from './loading-indicator.react.js';
import { MenuProvider } from './menu-provider.react.js';
-import AppsDirectory from './modals/apps/apps-directory.react.js';
+import AppsDirectory from './modals/apps/apps-directory-modal.react.js';
import UpdateModalHandler from './modals/update-modal.react.js';
import SettingsSwitcher from './navigation-panels/settings-switcher.react.js';
import Topbar from './navigation-panels/topbar.react.js';
import { updateNavInfoActionType } from './redux/action-types.js';
import DeviceIDUpdater from './redux/device-id-updater.js';
import DisconnectedBarVisibilityHandler from './redux/disconnected-bar-visibility-handler.js';
import DisconnectedBar from './redux/disconnected-bar.js';
import FocusHandler from './redux/focus-handler.react.js';
import PolicyAcknowledgmentHandler from './redux/policy-acknowledgment-handler.js';
import { useSelector } from './redux/redux-utils.js';
import VisibilityHandler from './redux/visibility-handler.react.js';
import history from './router-history.js';
import AccountSettings from './settings/account-settings.react.js';
import DangerZone from './settings/danger-zone.react.js';
import CommunityPicker from './sidebar/community-picker.react.js';
import Splash from './splash/splash.react.js';
import './typography.css';
import css from './style.css';
import getTitle from './title/getTitle.js';
import { type NavInfo } from './types/nav-types.js';
import { canonicalURLFromReduxState, navInfoFromURL } from './url-utils.js';
// 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>
<WagmiConfig client={wagmiClient}>
<WagmiENSCacheProvider>
<FocusHandler />
<VisibilityHandler />
<DeviceIDUpdater />
<PolicyAcknowledgmentHandler />
{content}
{this.props.modals}
</WagmiENSCacheProvider>
</WagmiConfig>
</MenuProvider>
</TooltipProvider>
</DndProvider>
);
}
onHeaderDoubleClick = () => electron?.doubleClickTopBar();
stopDoubleClickPropagation = electron ? e => e.stopPropagation() : null;
renderMainContent() {
const mainContent = this.getMainContentWithSwitcher();
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 />
<UpdateModalHandler />
<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>{mainContent}</InputStateContainer>
<div className={css.sidebar}>
<CommunityPicker />
</div>
</div>
);
}
getMainContentWithSwitcher() {
const { tab, settingsSection } = this.props.navInfo;
let mainContent;
if (tab === 'settings') {
if (settingsSection === 'account') {
mainContent = <AccountSettings />;
} else if (settingsSection === 'danger-zone') {
mainContent = <DangerZone />;
}
return (
<div className={css['main-content-container']}>
<div className={css.switcher}>
<SettingsSwitcher />
</div>
<div className={css['main-content']}>{mainContent}</div>
</div>
);
}
if (tab === 'calendar') {
mainContent = <Calendar url={this.props.location.pathname} />;
} else if (tab === 'chat') {
mainContent = <Chat />;
} else if (tab === 'apps') {
mainContent = <AppsDirectory />;
}
const mainContentClass = classnames(
css['main-content-container'],
css['main-content-container-column'],
);
return (
<div className={mainContentClass}>
<Topbar />
<div className={css['main-content']}>{mainContent}</div>
</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/modals/apps/apps-directory.react.js b/web/modals/apps/apps-directory-modal.react.js
similarity index 64%
rename from web/modals/apps/apps-directory.react.js
rename to web/modals/apps/apps-directory-modal.react.js
index 5fcfc3c4f..ecf539a1c 100644
--- a/web/modals/apps/apps-directory.react.js
+++ b/web/modals/apps/apps-directory-modal.react.js
@@ -1,56 +1,62 @@
// @flow
import * as React from 'react';
import { useSelector } from 'react-redux';
-import AppListing from './app-listing.react.js';
-import css from './apps.css';
+import { useModalContext } from 'lib/components/modal-provider.react.js';
+
+import AppListing from '../apps/app-listing.react.js';
+import css from '../apps/apps.css';
+import Modal from '../modal.react.js';
const APP_DIRECTORY_DATA = [
{
id: 'chat',
defaultEnabled: true,
readOnly: true,
name: 'Chat',
icon: 'message-square',
copy: 'Keep in touch with your community',
},
{
id: 'calendar',
defaultEnabled: true,
readOnly: false,
name: 'Calendar',
icon: 'calendar',
copy: 'Shared calendar for your community',
},
];
-function AppsDirectory(): React.Node {
+function AppsModal(): React.Node {
+ const { popModal } = useModalContext();
+
const enabledApps = useSelector(state => state.enabledApps);
const appData = React.useMemo(
() =>
APP_DIRECTORY_DATA.map(app => {
const { defaultEnabled, ...data } = app;
return {
...data,
enabled: enabledApps[app.id] ?? defaultEnabled,
};
}),
[enabledApps],
);
const appItems = React.useMemo(
() => appData.map(item => <AppListing key={item.id} {...item} />),
[appData],
);
return (
- <div className={css.appsDirectoryContainer}>
- <h4 className={css.appsHeader}>Choose Apps</h4>
- <div className={css.appsDirectoryList}>{appItems}</div>
- </div>
+ <Modal name="Choose apps" onClose={popModal} size="large">
+ <div className={css.appsDirectoryContainer}>
+ <div className={css.appsDirectoryList}>{appItems}</div>
+ </div>
+ </Modal>
);
}
-export default AppsDirectory;
+export default AppsModal;
diff --git a/web/modals/apps/apps.css b/web/modals/apps/apps.css
index c037c411f..6396ff43b 100644
--- a/web/modals/apps/apps.css
+++ b/web/modals/apps/apps.css
@@ -1,64 +1,67 @@
div.appsDirectoryContainer {
display: flex;
flex-direction: column;
- align-items: flex-start;
+ padding-top: 34px;
+ padding-bottom: 16px;
}
h4.appsHeader {
color: var(--fg);
padding: 20px 0 40px 40px;
font-weight: var(--semi-bold);
}
div.appsDirectoryList {
- margin-left: 20px;
display: flex;
flex-direction: column;
- row-gap: 10px;
+ row-gap: 16px;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 12px 32px;
}
div.appListingContainer {
color: var(--fg);
display: flex;
flex-direction: row;
- align-items: center;
+ align-items: space-between;
}
div.appListingTextContainer {
display: flex;
flex-direction: column;
flex: 1;
}
h5.appName {
font-weight: var(--semi-bold);
margin-bottom: 4px;
}
small.appCopy {
font-size: var(--xs-font-12);
}
-div.appListingIcon {
- padding: 0 20px;
+.appListingIcon {
+ padding: 0 20px 0 0;
align-self: stretch;
display: flex;
align-items: center;
}
.appListingIconState {
- padding: 0 20px;
+ padding: 0 0 0 20px;
font-size: var(--xl-font-20);
}
div.iconReadOnly {
color: var(--app-list-icon-read-only-color);
}
.iconEnabled {
color: var(--app-list-icon-enabled-color);
}
.iconDisabled {
color: var(--app-list-icon-disabled-color);
}
diff --git a/web/navigation-panels/topbar.react.js b/web/navigation-panels/topbar.react.js
index c491db3f3..42230f9c4 100644
--- a/web/navigation-panels/topbar.react.js
+++ b/web/navigation-panels/topbar.react.js
@@ -1,46 +1,42 @@
// @flow
import * as React from 'react';
-import { useDispatch } from 'react-redux';
+import { useModalContext } from 'lib/components/modal-provider.react.js';
import SWMansionIcon from 'lib/components/SWMansionIcon.react.js';
import AppSwitcher from './app-switcher.react.js';
import css from './topbar.css';
import Button from '../components/button.react.js';
-import { updateNavInfoActionType } from '../redux/action-types.js';
+import AppsDirectory from '../modals/apps/apps-directory-modal.react.js';
function Topbar(): React.Node {
- const dispatch = useDispatch();
-
- const onClickApps = React.useCallback(() => {
- dispatch({
- type: updateNavInfoActionType,
- payload: {
- tab: 'apps',
- },
- });
- }, [dispatch]);
+ const { pushModal } = useModalContext();
+
+ const onClickApps = React.useCallback(
+ () => pushModal(<AppsDirectory />),
+ [pushModal],
+ );
const appNavigationItem = React.useMemo(
() => (
<Button className={css.plusButton} onClick={onClickApps}>
<SWMansionIcon icon="plus-small" size={24} />
</Button>
),
[onClickApps],
);
return (
<div className={css.container}>
<div className={css.tabs}>
<AppSwitcher />
</div>
{appNavigationItem}
</div>
);
}
const MemoizedTopbar: React.ComponentType<{}> = React.memo<{}>(Topbar);
export default MemoizedTopbar;

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 23, 8:24 AM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2690640
Default Alt Text
(16 KB)

Event Timeline