Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3509803
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/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
Details
Attached
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)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment