Repository: busyorg/busy
Pull request #2090 (2018-07-25 09:21:24)

Nightmode Preparations
Fixes part of #2065 

### Changes

These changes are the first step for nightmode implementation. After that only CSS work is necessary.

* Added a settings option for nightmode (incl. german translation)
* added class `dark` to layout container when nightmode is enabled
* added nightmode css for Topnav

What's missing is maybe a toggle button somewhere in the navbar.

### Test plan

* [x] Go to settings page, enable new nightmode option and save
* [x] See in dev tools: main layout container `ant-layout` now has an additional class `dark`

### Demo

![busy-nightmode-setting](https://user-images.githubusercontent.com/6792578/43194577-fab6b850-9002-11e8-9daf-98820fea1fd2.gif)


mktcode: Can someone explain why those tests fail? I had a fresh fork and created a new branch from develop.
Sekhmet: Thanks a lot. Seems to be working fine. Next step is to create styles for dark theme.

Patch:
diff --git a/src/client/Wrapper.js b/src/client/Wrapper.js
index 33686c91..c424a40f 100644
--- a/src/client/Wrapper.js
+++ b/src/client/Wrapper.js
@@ -17,6 +17,7 @@ import {
   getUsedLocale,
   getTranslations,
   getUseBeta,
+  getNightmode,
 } from './reducers';
 import { login, logout, busyLogin } from './auth/authActions';
 import { getFollowing, getNotifications } from './user/userActions';
@@ -44,6 +45,7 @@ import BBackTop from './components/BBackTop';
     usedLocale: getUsedLocale(state),
     translations: getTranslations(state),
     locale: getLocale(state),
+    nightmode: getNightmode(state),
   }),
   {
     login,
@@ -77,6 +79,7 @@ export default class Wrapper extends React.PureComponent {
     getNotifications: PropTypes.func,
     setUsedLocale: PropTypes.func,
     busyLogin: PropTypes.func,
+    nightmode: PropTypes.bool,
   };
 
   static defaultProps = {
@@ -93,6 +96,7 @@ export default class Wrapper extends React.PureComponent {
     getNotifications: () => {},
     setUsedLocale: () => {},
     busyLogin: () => {},
+    nightmode: false,
   };
 
   static async fetchData({ store, req, res }) {
@@ -201,14 +205,17 @@ export default class Wrapper extends React.PureComponent {
   }
 
   render() {
-    const { user, usedLocale, translations } = this.props;
+    const { user, usedLocale, translations, nightmode } = this.props;
 
     const language = findLanguage(usedLocale);
 
     return (
       <IntlProvider key={language.id} locale={language.localeData} messages={translations}>
         <LocaleProvider locale={enUS}>
-          <Layout data-dir={language && language.rtl ? 'rtl' : 'ltr'}>
+          <Layout
+            className={nightmode ? 'dark' : ''}
+            data-dir={language && language.rtl ? 'rtl' : 'ltr'}
+          >
             <Layout.Header style={{ position: 'fixed', width: '100%', zIndex: 1050 }}>
               <Topnav username={user.name} onMenuItemClick={this.handleMenuItemClick} />
             </Layout.Header>
diff --git a/src/client/components/Navigation/Topnav-dark.less b/src/client/components/Navigation/Topnav-dark.less
new file mode 100644
index 00000000..10e68f5b
--- /dev/null
+++ b/src/client/components/Navigation/Topnav-dark.less
@@ -0,0 +1,37 @@
+@import (reference) '../../styles/modules/variables.less';
+@import (reference) '../../styles/custom.less';
+
+@base-spacing: 0;
+
+.dark {
+  .Topnav {
+    background-color: @grey-nero;
+    border-bottom: @normal-grey;
+
+    &__input-container {
+      input {
+        color: @white-smoke;
+        border: @border-width-base @border-style-base @normal-grey;
+
+        &:hover,
+        &:focus {
+          border: @border-width-base @border-style-base @normal-grey !important;
+        }
+      }
+    }
+
+    &__menu-container {
+      &__menu {
+        & > li.ant-menu-item {
+          & > a {
+            color: @tertiary-color !important;
+
+            &:hover {
+              color: @white-smoke !important;
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/src/client/components/Navigation/Topnav.less b/src/client/components/Navigation/Topnav.less
index 6af210c5..817fd36d 100644
--- a/src/client/components/Navigation/Topnav.less
+++ b/src/client/components/Navigation/Topnav.less
@@ -286,3 +286,5 @@ div[data-dir='rtl'] .Topnav {
   width: 16px !important;
   justify-content: flex-end !important;
 }
+
+@import 'Topnav-dark';
diff --git a/src/client/locales/de-DE.json b/src/client/locales/de-DE.json
index 5a2f3ce0..f450e26e 100644
--- a/src/client/locales/de-DE.json
+++ b/src/client/locales/de-DE.json
@@ -314,6 +314,9 @@
   "nsfw_posts": "NSFW Beiträge",
   "display_nsfw_posts_details": "Sie können alle Beiträge mit dem Stichwort NSFW standardmäßig anzeigen lassen.",
   "display_nsfw_posts": "NSFW Beiträge anzeigen",
+  "nightmode": "Nachtmodus",
+  "nightmode_details": "Aktiviere diese Option, für eine augenschonendere Oberfläche bei Nacht.",
+  "use_nightmode": "Nachtmodus aktivieren",
   "rewrite_links": "Erneuern sie ihre Links",
   "rewrite_links_details": "Sie können diese Option aktivieren, um Steemit.com Links mit Busy.org Links zu ersetzen.",
   "use_beta": "Verwenden sie die Busy-Betaversion",
diff --git a/src/client/locales/default.json b/src/client/locales/default.json
index d101e7f5..6a27b126 100644
--- a/src/client/locales/default.json
+++ b/src/client/locales/default.json
@@ -316,6 +316,9 @@
   "nsfw_posts": "NSFW Posts",
   "display_nsfw_posts_details": "You can enable all posts tagged with NSFW to be shown as default.",
   "display_nsfw_posts": "Display NSFW Posts",
+  "nightmode": "Nightmode",
+  "nightmode_details": "You can enable this option for a more eye-friendly experience at night.",
+  "use_nightmode": "Use Nightmode",
   "rewrite_links": "Rewrite links",
   "rewrite_links_details": "You can enable this option to replace Steemit.com links with Busy.org links.",
   "use_beta": "Use Busy beta",
diff --git a/src/client/reducers.js b/src/client/reducers.js
index 97d16ac8..8df34b7b 100644
--- a/src/client/reducers.js
+++ b/src/client/reducers.js
@@ -122,6 +122,7 @@ export const getLocale = state => fromSettings.getLocale(state.settings);
 export const getVotingPower = state => fromSettings.getVotingPower(state.settings);
 export const getVotePercent = state => fromSettings.getVotePercent(state.settings);
 export const getShowNSFWPosts = state => fromSettings.getShowNSFWPosts(state.settings);
+export const getNightmode = state => fromSettings.getNightmode(state.settings);
 export const getRewriteLinks = state => fromSettings.getRewriteLinks(state.settings);
 export const getUpvoteSetting = state => fromSettings.getUpvoteSetting(state.settings);
 export const getExitPageSetting = state => fromSettings.getExitPageSetting(state.settings);
diff --git a/src/client/settings/Settings.js b/src/client/settings/Settings.js
index 8cf31c6e..8a11cabf 100644
--- a/src/client/settings/Settings.js
+++ b/src/client/settings/Settings.js
@@ -11,6 +11,7 @@ import {
   getIsSettingsLoading,
   getVotePercent,
   getShowNSFWPosts,
+  getNightmode,
   getRewriteLinks,
   getUseBeta,
   getUpvoteSetting,
@@ -39,6 +40,7 @@ import packageJson from '../../../package.json';
     votingPower: getVotingPower(state),
     votePercent: getVotePercent(state),
     showNSFWPosts: getShowNSFWPosts(state),
+    nightmode: getNightmode(state),
     rewriteLinks: getRewriteLinks(state),
     useBeta: getUseBeta(state),
     loading: getIsSettingsLoading(state),
@@ -56,6 +58,7 @@ export default class Settings extends React.Component {
     votePercent: PropTypes.number,
     loading: PropTypes.bool,
     showNSFWPosts: PropTypes.bool,
+    nightmode: PropTypes.bool,
     rewriteLinks: PropTypes.bool,
     useBeta: PropTypes.bool,
     reload: PropTypes.func,
@@ -72,6 +75,7 @@ export default class Settings extends React.Component {
     votePercent: 10000,
     loading: false,
     showNSFWPosts: false,
+    nightmode: false,
     rewriteLinks: false,
     useBeta: false,
     upvoteSetting: true,
@@ -91,6 +95,7 @@ export default class Settings extends React.Component {
     votingPower: 'auto',
     votePercent: 10000,
     showNSFWPosts: false,
+    nightmode: false,
     rewriteLinks: false,
     exitPageSetting: true,
   };
@@ -101,6 +106,7 @@ export default class Settings extends React.Component {
       votingPower: this.props.votingPower,
       votePercent: this.props.votePercent / 100,
       showNSFWPosts: this.props.showNSFWPosts,
+      nightmode: this.props.nightmode,
       rewriteLinks: this.props.rewriteLinks,
       useBeta: this.props.useBeta,
       upvoteSetting: this.props.upvoteSetting,
@@ -129,6 +135,10 @@ export default class Settings extends React.Component {
       this.setState({ showNSFWPosts: nextProps.showNSFWPosts });
     }
 
+    if (nextProps.nightmode !== this.props.nightmode) {
+      this.setState({ nightmode: nextProps.nightmode });
+    }
+
     if (nextProps.rewriteLinks !== this.props.rewriteLinks) {
       this.setState({ rewriteLinks: nextProps.rewriteLinks });
     }
@@ -153,6 +163,7 @@ export default class Settings extends React.Component {
         votingPower: this.state.votingPower,
         votePercent: this.state.votePercent * 100,
         showNSFWPosts: this.state.showNSFWPosts,
+        nightmode: this.state.nightmode,
         rewriteLinks: this.state.rewriteLinks,
         useBeta: this.state.useBeta,
         upvoteSetting: this.state.upvoteSetting,
@@ -170,6 +181,7 @@ export default class Settings extends React.Component {
   handleVotingPowerChange = event => this.setState({ votingPower: event.target.value });
   handleVotePercentChange = value => this.setState({ votePercent: value });
   handleShowNSFWPosts = event => this.setState({ showNSFWPosts: event.target.checked });
+  handleNightmode = event => this.setState({ nightmode: event.target.checked });
   handleRewriteLinksChange = event => this.setState({ rewriteLinks: event.target.checked });
   handleUseBetaChange = event => this.setState({ useBeta: event.target.checked });
   handleExitPageSettingChange = event => this.setState({ exitPageSetting: event.target.checked });
@@ -185,12 +197,14 @@ export default class Settings extends React.Component {
       locale: initialLocale,
       votingPower: initialVotingPower,
       showNSFWPosts: initialShowNSFWPosts,
+      nightmode: initialNightmode,
       loading,
     } = this.props;
     const {
       votingPower,
       locale,
       showNSFWPosts,
+      nightmode,
       rewriteLinks,
       useBeta,
       upvoteSetting,
@@ -317,6 +331,27 @@ export default class Settings extends React.Component {
                     </Checkbox>
                   </div>
                 </div>
+                <div className="Settings__section">
+                  <h3>
+                    <FormattedMessage id="nightmode" defaultMessage="Nightmode" />
+                  </h3>
+                  <p>
+                    <FormattedMessage
+                      id="nightmode_details"
+                      defaultMessage="You can enable this option for a more eye-friendly experience at night."
+                    />
+                  </p>
+                  <div className="Settings__section__checkbox">
+                    <Checkbox
+                      name="nightmode"
+                      defaultChecked={initialNightmode}
+                      checked={nightmode}
+                      onChange={this.handleNightmode}
+                    >
+                      <FormattedMessage id="use_nightmode" defaultMessage="Use Nightmode" />
+                    </Checkbox>
+                  </div>
+                </div>
                 <div className="Settings__section">
                   <h3>
                     <FormattedMessage id="rewrite_links" defaultMessage="Rewrite links" />
diff --git a/src/client/settings/settingsReducer.js b/src/client/settings/settingsReducer.js
index 6c2b68a7..9c355a0c 100644
--- a/src/client/settings/settingsReducer.js
+++ b/src/client/settings/settingsReducer.js
@@ -7,6 +7,7 @@ const initialState = {
   votingPower: 'auto',
   votePercent: 10000,
   showNSFWPosts: false,
+  nightmode: false,
   rewriteLinks: false,
   loading: false,
   upvoteSetting: true,
@@ -26,6 +27,7 @@ const settings = (state = initialState, action) => {
           votingPower,
           votePercent,
           showNSFWPosts,
+          nightmode,
           rewriteLinks,
           upvoteSetting,
           exitPageSetting,
@@ -38,6 +40,7 @@ const settings = (state = initialState, action) => {
           votingPower: votingPower || initialState.votingPower,
           votePercent: votePercent || initialState.votePercent,
           showNSFWPosts: showNSFWPosts || initialState.showNSFWPosts,
+          nightmode: nightmode || initialState.nightmode,
           rewriteLinks:
             typeof rewriteLinks === 'boolean' ? rewriteLinks : initialState.rewriteLinks,
           upvoteSetting:
@@ -62,6 +65,7 @@ const settings = (state = initialState, action) => {
         votingPower: action.payload.votingPower,
         votePercent: action.payload.votePercent,
         showNSFWPosts: action.payload.showNSFWPosts,
+        nightmode: action.payload.nightmode,
         rewriteLinks: action.payload.rewriteLinks,
         upvoteSetting: action.payload.upvoteSetting,
         exitPageSetting: action.payload.exitPageSetting,
@@ -90,6 +94,7 @@ export const getLocale = state => state.locale;
 export const getVotingPower = state => state.votingPower;
 export const getVotePercent = state => state.votePercent;
 export const getShowNSFWPosts = state => state.showNSFWPosts;
+export const getNightmode = state => state.nightmode;
 export const getRewriteLinks = state => !!state.rewriteLinks;
 export const getUpvoteSetting = state => state.upvoteSetting;
 export const getExitPageSetting = state => state.exitPageSetting;


Test patch:
diff --git a/src/client/settings/__tests__/settingsReducer.js b/src/client/settings/__tests__/settingsReducer.js
index 20d158e0..81bdc42e 100644
--- a/src/client/settings/__tests__/settingsReducer.js
+++ b/src/client/settings/__tests__/settingsReducer.js
@@ -11,6 +11,7 @@ describe('settingsReducer', () => {
     votePercent: 10000,
     loading: false,
     showNSFWPosts: false,
+    nightmode: false,
     rewriteLinks: false,
     upvoteSetting: true,
     exitPageSetting: true,
@@ -63,7 +64,7 @@ describe('settingsReducer', () => {
     expect(settingsReducer(stateBefore, action)).to.eql(stateAfter);
   });
 
-  it('should set locale, voting power, vote percent, loading, showNSFWPosts, and rewriteLinks after saving succeeded', () => {
+  it('should set locale, voting power, vote percent, loading, showNSFWPosts, nightmode and rewriteLinks after saving succeeded', () => {
     const stateBefore = {
       ...initialState,
       loading: true,
@@ -75,6 +76,7 @@ describe('settingsReducer', () => {
       votePercent: 10000,
       votingPower: 'on',
       showNSFWPosts: true,
+      nightmode: true,
       rewriteLinks: true,
     };
     const action = {
@@ -84,6 +86,7 @@ describe('settingsReducer', () => {
         votingPower: 'on',
         votePercent: 10000,
         showNSFWPosts: true,
+        nightmode: true,
         rewriteLinks: true,
         upvoteSetting: true,
         exitPageSetting: true,