diff --git a/web/app.react.js b/web/app.react.js index 73fc0ff50..77c768214 100644 --- a/web/app.react.js +++ b/web/app.react.js @@ -1,322 +1,346 @@ // @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 AppsDirectory from './apps/apps-directory.react.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 UpdateModalHandler from './modals/update-modal.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 LeftLayoutAside from './sidebar/left-layout-aside.react.js'; +import SettingsSwitcher from './sidebar/settings-switcher.react.js'; import Splash from './splash/splash.react.js'; import './typography.css'; import css from './style.css'; import getTitle from './title/getTitle.js'; +import Topbar from './topbar/topbar.react.js'; import { type NavInfo } from './types/nav-types.js'; import { canonicalURLFromReduxState, navInfoFromURL } from './url-utils.js'; import { WagmiENSCacheProvider, wagmiClient } from './utils/wagmi-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() { - let mainContent; - const { tab, settingsSection } = this.props.navInfo; - if (tab === 'calendar') { - mainContent = ; - } else if (tab === 'chat') { - mainContent = ; - } else if (tab === 'apps') { - mainContent = ; - } else if (tab === 'settings') { - if (settingsSection === 'account') { - mainContent = ; - } else if (settingsSection === 'danger-zone') { - mainContent = ; - } - } + 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}
-
-
+ {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/calendar/filter-panel.css b/web/calendar/filter-panel.css index 567675798..62789b9cb 100644 --- a/web/calendar/filter-panel.css +++ b/web/calendar/filter-panel.css @@ -1,167 +1,167 @@ div.container { position: fixed; width: 300px; - top: 62px; + top: 126px; bottom: 0; background-attachment: fixed; display: flex; flex-direction: column; } div.filters { color: #dddddd; overflow-y: auto; height: 100%; } div.option { margin: 5px 18px 5px 32px; } div.optionThread { min-height: 20px; position: relative; display: flex; align-items: center; justify-content: flex-start; } div.optionThread input { position: absolute; left: 0; top: 0; width: calc(1em + 8px); height: calc(1em + 8px); z-index: 2; opacity: 0; cursor: pointer; } div.optionThread label { font-weight: 600 !important; font-size: 15px; padding-left: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: inline-block; text-indent: 24px; } div.optionCheckbox { display: block; content: ''; width: calc(1em + 2px); height: calc(1em + 2px); border: 1px solid transparent; z-index: 0; position: absolute; left: 0; top: 0; } div.checkboxAfterOption { -webkit-transform: scale(0.6); -ms-transform: scale(0.6); transform: scale(0.6); } a.only { color: #888888; padding-left: 5px; font-size: 13px; cursor: pointer; display: none; } div.option:hover a.only { display: inline; } a.only:hover { text-decoration: underline; color: #eeeeee; } a.settingsCog { display: none; margin-left: auto; padding-left: 5px; cursor: pointer; color: #aaaaaa; } div.option:hover a.settingsCog { display: inline; } a.settingsCog:hover { color: #eeeeee; } div.optionDetails { font-style: italic; color: #888888; font-size: 13px; display: inline-block; text-indent: 24px; padding-left: 10px; padding-top: 1px; } div.searchContainer { padding: 8px 10px; background-color: rgba(0, 0, 0, 0.89); } div.search { display: flex; border-radius: 5px; padding: 5px; background-color: rgba(255, 255, 255, 0.27); align-items: center; } svg.searchVector { fill: white; height: 22px; width: 22px; padding: 0 3px; } div.search > input { color: white; padding: 0; border: none; background-color: transparent; font-weight: 600; font-size: 15px; flex-grow: 1; margin-left: 3px; } div.search > input:focus { outline: none; } svg.clearQuery { font-size: 15px; padding-bottom: 1px; padding-right: 2px; color: #cccccc; } svg.clearQuery:hover { font-size: 15px; padding-bottom: 1px; padding-right: 2px; color: white; } div.noResults { font-style: italic; text-align: center; font-size: 15px; color: #888888; } a.collapse { margin-left: auto; padding-left: 5px; cursor: pointer; color: #aaaaaa; } div.category { margin: 5px 18px 8px 18px; } div.extras { background-color: rgba(0, 0, 0, 0.89); color: #dddddd; padding: 8px 20px; font-size: 14px; display: flex; } div.extras > label { display: flex; align-items: center; } div.extras > label > span { margin-left: 8px; } diff --git a/web/style.css b/web/style.css index 35187733f..f10b98700 100644 --- a/web/style.css +++ b/web/style.css @@ -1,218 +1,225 @@ *, *:before, *:after { padding: 0; margin: 0; -ms-overflow-style: -ms-autohiding-scrollbar; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } html { height: 100%; font-size: 62.5%; } body { font-family: var(--font-stack); background: var(--bg); height: 100%; overflow: hidden; font-size: 1.6rem; } a { text-decoration: none; color: #2a5db0; cursor: pointer; } button { cursor: pointer; } img, iframe { display: block; } input[type='text'], input[type='password'], textarea { -webkit-appearance: none; -moz-appearance: none; -webkit-border-radius: 0; border: 1px solid #dddddd; border-radius: 1px; font-family: var(--font-stack); } button svg { vertical-align: top; } :global(#react-root) { display: flex; flex-direction: column; height: 100%; } div.layout { height: 100vh; display: grid; grid-template-columns: 400px repeat(12, 1fr); grid-template-rows: 65px calc(100vh - 65px); grid-template-areas: 'nav nav nav nav nav nav nav nav nav nav nav nav nav' 'sBar app app app app app app app app app app app app'; } header.header { background: var(--bg); z-index: 1; grid-area: nav; } div.main-header { height: 64px; background: var(--bg); display: flex; align-items: center; border-bottom: 1px solid var(--border-color); } div.main-header > .wordmark { color: var(--fg); padding-left: 92px; font-family: var(--font-logo); line-height: var(--line-height-text); font-weight: var(--semi-bold); font-size: var(--logo-font-22); margin-right: 40px; } .wordmark > a { color: inherit; } .electron-draggable { -webkit-app-region: drag; } .electron-non-draggable { -webkit-app-region: no-drag; } div.main-content-container { position: relative; grid-area: app; display: flex; overflow: hidden; } +div.main-content-container-column { + flex-direction: column; +} div.main-content { display: flex; flex: 1; overflow: hidden; } +.switcher { + border-right: 1px solid var(--border-color); +} + div.upper-right { position: absolute; top: 0; right: 0; padding: 15px 16px; } span.loading-indicator-loading { display: inline-block; } @keyframes loading-indicator-loading { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } span.loading-indicator-loading-medium:after { content: ' '; display: block; width: 15px; height: 15px; border-radius: 50%; border: 3px solid #fff; border-color: #fff transparent #fff transparent; animation-name: loading-indicator-loading; animation-duration: 1.2s; animation-iteration-count: infinite; animation-timing-function: linear; } span.loading-indicator-loading-large:after { content: ' '; display: block; width: 25px; height: 25px; border-radius: 50%; border: 3px solid #fff; border-color: #fff transparent #fff transparent; animation-name: loading-indicator-loading; animation-duration: 1.2s; animation-iteration-count: infinite; animation-timing-function: linear; } span.loading-indicator-loading-small:after { content: ' '; display: block; width: 9px; height: 9px; border-radius: 50%; border: 2px solid #fff; border-color: #fff transparent #fff transparent; animation-name: loading-indicator-loading; animation-duration: 1.2s; animation-iteration-count: infinite; animation-timing-function: linear; } span.loading-indicator-black:after { border-color: #000 transparent #000 transparent; } span.loading-indicator-error { font-weight: bold; color: white; line-height: 0; } span.loading-indicator-error-black { font-weight: bold; color: red; line-height: 0; } .hidden { display: none; } .italic { font-style: italic; } span.page-loading { margin-top: 5px; margin-right: 12px; float: left; } span.page-error { margin: 15px; font-size: 42px; float: left; color: red; } @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 320dpi), only screen and (min-resolution: 2dppx) { header.header, header.main-header, div.splash-header-container, div.splash-top-container, div.splash-bottom, div.calendar-filters-container { background: var(--bg); } } @media (hover: none) { div.splash-header-container, div.splash-top-container, div.splash-bottom { background-attachment: initial; } }