diff --git a/web/app.react.js b/web/app.react.js index 55a41bc7a..a007d54c0 100644 --- a/web/app.react.js +++ b/web/app.react.js @@ -1,247 +1,251 @@ // @flow import '@fontsource/inter'; import '@fontsource/inter/500.css'; import '@fontsource/inter/600.css'; import '@fontsource/ibm-plex-sans'; import '@fontsource/ibm-plex-sans/500.css'; import '@fontsource/ibm-plex-sans/600.css'; import 'basscss/css/basscss.min.css'; import './theme.css'; import { config as faConfig } from '@fortawesome/fontawesome-svg-core'; 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 { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors'; import { mostRecentReadThreadSelector } 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 Calendar from './calendar/calendar.react'; import Chat from './chat/chat.react'; import InputStateContainer from './input/input-state-container.react'; import LoadingIndicator from './loading-indicator.react'; +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 LeftLayoutAside from './sidebar/left-layout-aside.react'; import Splash from './splash/splash.react'; import './typography.css'; import css from './style.css'; import { type NavInfo, updateNavInfoActionType } 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, +mostRecentReadThread: ?string, +activeThreadCurrentlyUnread: boolean, // Redux dispatch functions +dispatch: Dispatch, }; type State = { +currentModal: ?React.Node, }; class App extends React.PureComponent { state: State = { currentModal: null, }; 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); } } } render() { let content; if (this.props.loggedIn) { content = this.renderMainContent(); } else { content = ( ); } return ( {content} {this.state.currentModal} ); } renderMainContent() { let mainContent; if (this.props.navInfo.tab === 'calendar') { mainContent = ( ); } else if (this.props.navInfo.tab === 'chat') { mainContent = ; } return (
+ +

Comm

{mainContent}
); } setModal = (modal: ?React.Node) => { this.setState({ currentModal: modal }); }; clearModal() { this.setModal(null); } } 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 mostRecentReadThread = useSelector(mostRecentReadThreadSelector); const activeThreadCurrentlyUnread = useSelector( state => !activeChatThreadID || !!state.threadStore.threadInfos[activeChatThreadID]?.currentUser.unread, ); const dispatch = useDispatch(); return ( ); }, ); export default ConnectedApp; diff --git a/web/redux/disconnected-bar-visibility-handler.js b/web/redux/disconnected-bar-visibility-handler.js new file mode 100644 index 000000000..a3b6d4ddd --- /dev/null +++ b/web/redux/disconnected-bar-visibility-handler.js @@ -0,0 +1,33 @@ +// @flow + +import * as React from 'react'; + +import { useDisconnectedBarVisibilityHandler } from 'lib/hooks/disconnected-bar'; + +function useNetworkConnected() { + const [networkConnected, setNetworkConnected] = React.useState(true); + React.useEffect(() => { + if (!window) { + return undefined; + } + const handleOnline = () => setNetworkConnected(true); + const handleOffline = () => setNetworkConnected(false); + + window.addEventListener('online', handleOnline); + window.addEventListener('offline', handleOffline); + return () => { + window.removeEventListener('online', handleOnline); + window.removeEventListener('offline', handleOffline); + }; + }, []); + + return networkConnected; +} + +function DisconnectedBarVisibilityHandler(): null { + const networkConnected = useNetworkConnected(); + useDisconnectedBarVisibilityHandler(networkConnected); + return null; +} + +export default DisconnectedBarVisibilityHandler; diff --git a/web/redux/disconnected-bar.css b/web/redux/disconnected-bar.css new file mode 100644 index 000000000..dd234d738 --- /dev/null +++ b/web/redux/disconnected-bar.css @@ -0,0 +1,19 @@ +p.bar { + position: fixed; + z-index: 2; + left: 50%; + padding: 0.5rem 2rem; + transform: translateX(-50%); + font-weight: var(--semi-bold); + font-size: var(--m-font-16); +} + +p.connecting { + background-color: var(--disconnected-bar-connecting-bg); + color: var(--disconnected-bar-connecting-color); +} + +p.disconnected { + background-color: var(--disconnected-bar-alert-bg); + color: var(--disconnected-bar-alert-color); +} diff --git a/web/redux/disconnected-bar.js b/web/redux/disconnected-bar.js new file mode 100644 index 000000000..41af4fa9d --- /dev/null +++ b/web/redux/disconnected-bar.js @@ -0,0 +1,31 @@ +// @flow + +import classNames from 'classnames'; +import * as React from 'react'; + +import { + useShouldShowDisconnectedBar, + useDisconnectedBar, +} from 'lib/hooks/disconnected-bar'; + +import css from './disconnected-bar.css'; + +function DisconnectedBar(): React.Node { + const { shouldShowDisconnectedBar } = useShouldShowDisconnectedBar(); + const [showing, setShowing] = React.useState(shouldShowDisconnectedBar); + + const barCause = useDisconnectedBar(setShowing); + const isDisconnected = barCause === 'disconnected'; + const text = isDisconnected ? 'DISCONNECTED' : 'CONNECTING…'; + if (!showing) { + return null; + } + + const textClasses = classNames(css.bar, { + [css.disconnected]: isDisconnected, + [css.connecting]: !isDisconnected, + }); + return

{text}

; +} + +export default DisconnectedBar; diff --git a/web/theme.css b/web/theme.css index c8b4c9bf6..047a3190c 100644 --- a/web/theme.css +++ b/web/theme.css @@ -1,93 +1,97 @@ :root { /* Never use color values defined here directly in CSS. Add color variables to "Color Theme" below The reason we never use color values defined here directly in CSS is 1. It makes changing themes from light / dark / user generated impossible. 2. Gives the programmer context into the color being used. 3. If our color system changes it's much easier to change color values in one place. Add a color value to the theme below, and then use it in your CSS. naming convention: - bg: background. - fg: foreground. - color: text-color */ --shades-white-100: #ffffff; --shades-white-90: #f5f5f5; --shades-white-80: #ebebeb; --shades-white-70: #e0e0e0; --shades-white-60: #cccccc; --shades-black-100: #0a0a0a; --shades-black-90: #1f1f1f; --shades-black-80: #404040; --shades-black-70: #666666; --shades-black-60: #808080; --violet-dark-100: #7e57c2; --violet-dark-80: #6d49ab; --violet-dark-60: #563894; --violet-dark-40: #44297a; --violet-dark-20: #331f5c; --violet-light-100: #ae94db; --violet-light-80: #b9a4df; --violet-light-60: #d3c6ec; --violet-light-40: #e8e0f5; --violet-light-20: #f3f0fa; --success-light-10: #d5f6e3; --success-light-50: #6cdf9c; --success-primary: #00c853; --success-dark-50: #029841; --success-dark-90: #034920; --error-light-10: #feebe6; --error-light-50: #f9947b; --error-primary: #f53100; --error-dark-50: #b62602; --error-dark-90: #4f1203; --bg: var(--shades-black-100); --fg: var(--shades-white-100); --color-disabled: var(--shades-black-60); --text-input-bg: var(--shades-black-80); --text-input-color: var(--shades-white-60); --text-input-placeholder: var(--shades-white-60); --border: var(--shades-black-80); --error: var(--error-primary); --success: var(--success-dark-50); /* Color Theme */ --btn-bg-primary: var(--violet-dark-100); --chat-bg: var(--violet-dark-80); --chat-confirmation-icon: var(--violet-dark-100); --keyserver-selection: var(--violet-dark-60); --thread-selection: var(--violet-light-80); --selected-thread-bg: var(--shades-black-90); --chat-timestamp-color: var(--shades-black-60); --tool-tip-bg: var(--shades-black-80); --tool-tip-color: var(--shades-black-60); --border-color: var(--shades-black-60); --calendar-chevron: var(--shades-black-60); --calendar-day-bg: var(--shades-black-60); --calendar-day-selected-color: var(--violet-dark-80); --community-bg: var(--shades-black-90); --unread-bg: var(--error-primary); --settings-btn-bg: var(--violet-dark-100); --modal-bg: var(--shades-black-90); --join-bg: var(--shades-black-90); --help-color: var(--shades-black-60); --modal-bg: var(--shades-black-90); --breadcrumb-color: var(--shades-black-60); --breadcrumb-color-unread: var(--shades-white-60); --join-bg: var(--shades-black-90); --btn-secondary-border: var(--shades-black-60); --thread-color-read: var(--shades-black-60); --thread-from-color-read: var(--shades-black-80); --thread-last-message-color-read: var(--shades-black-60); --relationship-button-green: var(--success-dark-50); --relationship-button-red: var(--error-primary); --relationship-button-text: var(--fg); + --disconnected-bar-alert-bg: var(--error-dark-50); + --disconnected-bar-alert-color: var(--shades-white-100); + --disconnected-bar-connecting-bg: var(--shades-white-70); + --disconnected-bar-connecting-color: var(--shades-black-100); }