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, }; class App extends React.PureComponent { 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 = ; } return ( {content} {this.props.modals} ); } onHeaderDoubleClick = () => electron?.doubleClickTopBar(); stopDoubleClickPropagation = electron ? e => e.stopPropagation() : null; renderMainContent() { const mainContent = this.getMainContentWithSwitcher(); let navigationArrows = null; if (electron) { navigationArrows = ; } const headerClasses = classnames({ [css.header]: true, [css['electron-draggable']]: electron, }); const wordmarkClasses = classnames({ [css.wordmark]: true, [css['electron-non-draggable']]: electron, }); return (

Comm

{navigationArrows}
{mainContent}
); } getMainContentWithSwitcher() { const { tab, settingsSection } = this.props.navInfo; let mainContent; if (tab === 'settings') { if (settingsSection === 'account') { mainContent = ; } else if (settingsSection === 'danger-zone') { mainContent = ; } return (
{mainContent}
); } if (tab === 'calendar') { mainContent = ; } else if (tab === 'chat') { mainContent = ; } else if (tab === 'apps') { mainContent = ; } const mainContentClass = classnames( css['main-content-container'], css['main-content-container-column'], ); return (
{mainContent}
); } } const fetchEntriesLoadingStatusSelector = createLoadingStatusSelector( fetchEntriesActionTypes, ); const updateCalendarQueryLoadingStatusSelector = createLoadingStatusSelector( updateCalendarQueryActionTypes, ); const ConnectedApp: React.ComponentType = React.memo( 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]) => ( {modal} )), [modalContext.modals], ); return ( ); }, ); function AppWithProvider(props: BaseProps): React.Node { return ( ); } 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 => ), [appData], ); return ( -
-

Choose Apps

-
{appItems}
-
+ +
+
{appItems}
+
+
); } -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(), + [pushModal], + ); const appNavigationItem = React.useMemo( () => ( ), [onClickApps], ); return (
{appNavigationItem}
); } const MemoizedTopbar: React.ComponentType<{}> = React.memo<{}>(Topbar); export default MemoizedTopbar;