diff --git a/web/app-theme-wrapper.react.js b/web/app-theme-wrapper.react.js
new file mode 100644
index 000000000..95668ca71
--- /dev/null
+++ b/web/app-theme-wrapper.react.js
@@ -0,0 +1,30 @@
+// @flow
+
+import * as React from 'react';
+
+import { useSelector } from './redux/redux-utils.js';
+import css from './style.css';
+
+type Props = {
+ +children: React.Node,
+};
+
+function AppThemeWrapper(props: Props): React.Node {
+ const { children } = props;
+
+ const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme);
+ const theme = activeTheme ? activeTheme : 'light';
+
+ const appThemeWrapper = React.useMemo(
+ () => (
+
+ {children}
+
+ ),
+ [children, theme],
+ );
+
+ return appThemeWrapper;
+}
+
+export default AppThemeWrapper;
diff --git a/web/app.react.js b/web/app.react.js
index 76d24d54b..b60735689 100644
--- a/web/app.react.js
+++ b/web/app.react.js
@@ -1,390 +1,393 @@
// @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 { ChatMentionContextProvider } from 'lib/components/chat-mention-provider.react.js';
import { EditUserAvatarProvider } from 'lib/components/edit-user-avatar-provider.react.js';
import {
ModalProvider,
useModalContext,
} from 'lib/components/modal-provider.react.js';
import { webAndKeyserverCodeVersion } from 'lib/facts/version.js';
import {
createLoadingStatusSelector,
combineLoadingStatuses,
} from 'lib/selectors/loading-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 { infoFromURL } from 'lib/utils/url-utils.js';
import { AlchemyENSCacheProvider, wagmiConfig } from 'lib/utils/wagmi-utils.js';
import QrCodeLogin from './account/qr-code-login.react.js';
+import AppThemeWrapper from './app-theme-wrapper.react.js';
import WebEditThreadAvatarProvider from './avatars/web-edit-thread-avatar-provider.react.js';
import Calendar from './calendar/calendar.react.js';
import Chat from './chat/chat.react.js';
import { EditModalProvider } from './chat/edit-message-provider.js';
import { TooltipProvider } from './chat/tooltip-provider.js';
import NavigationArrows from './components/navigation-arrows.react.js';
import { initOpaque } from './crypto/opaque-utils.js';
import electron from './electron.js';
import InputStateContainer from './input/input-state-container.react.js';
import InviteLinkHandler from './invite-links/invite-link-handler.react.js';
import InviteLinksRefresher from './invite-links/invite-links-refresher.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 SettingsSwitcher from './navigation-panels/settings-switcher.react.js';
import Topbar from './navigation-panels/topbar.react.js';
import useBadgeHandler from './push-notif/badge-handler.react.js';
import { PushNotificationsHandler } from './push-notif/push-notifs-handler.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 { persistConfig } from './redux/persist.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 { MessageSearchStateProvider } from './search/message-search-state-provider.react.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 { type NavInfo } from './types/nav-types.js';
import { canonicalURLFromReduxState, navInfoFromURL } from './url-utils.js';
initOpaque();
// 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,
setSessionIDOnRequest: true,
// Never reset the calendar range
calendarRangeInactivityLimit: null,
platformDetails: {
platform: electron?.platform ?? 'web',
codeVersion: webAndKeyserverCodeVersion,
stateVersion: persistConfig.version,
},
});
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 urlInfo = infoFromURL(pathname);
const newNavInfo = navInfoFromURL(urlInfo, { 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()}
{this.props.modals}
>
);
} else {
content = (
<>
{this.renderLoginPage()}
{this.props.modals}
>
);
}
return (
{content}
);
}
onHeaderDoubleClick = () => electron?.doubleClickTopBar();
stopDoubleClickPropagation = electron ? e => e.stopPropagation() : null;
renderLoginPage() {
const { loginMethod } = this.props.navInfo;
if (loginMethod === 'qr-code') {
return ;
}
return ;
}
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,
[css['wordmark-macos']]: electron?.platform === 'macos',
});
return (
);
}
getMainContentWithSwitcher() {
const { tab, settingsSection } = this.props.navInfo;
let mainContent;
if (tab === 'settings') {
if (settingsSection === 'account') {
mainContent = ;
} else if (settingsSection === 'danger-zone') {
mainContent = ;
}
return (
);
}
if (tab === 'calendar') {
mainContent = ;
} else if (tab === 'chat') {
mainContent = ;
}
const mainContentClass = classnames(
css['main-content-container'],
css['main-content-container-column'],
);
return (
);
}
}
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,
);
useBadgeHandler();
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/style.css b/web/style.css
index b5bca10aa..fd45706a9 100644
--- a/web/style.css
+++ b/web/style.css
@@ -1,232 +1,233 @@
*,
*: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) {
+:global(#react-root),
+div.appThemeContainer {
display: flex;
flex-direction: column;
height: 100%;
}
div.layout {
height: 100vh;
display: grid;
grid-template-columns: 300px 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: 34px;
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;
}
div.main-header > .wordmark-macos {
padding-left: 92px;
}
.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;
}
.sidebar {
display: flex;
grid-area: sBar;
}
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;
}
}