about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/mastodon/actions/rules.js27
-rw-r--r--app/javascript/mastodon/actions/server.js30
-rw-r--r--app/javascript/mastodon/components/account.js14
-rw-r--r--app/javascript/mastodon/components/display_name.js8
-rw-r--r--app/javascript/mastodon/components/server_banner.js91
-rw-r--r--app/javascript/mastodon/features/report/rules.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/compose_panel.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/report_modal.js4
-rw-r--r--app/javascript/mastodon/features/ui/index.js4
-rw-r--r--app/javascript/mastodon/initial_state.js1
-rw-r--r--app/javascript/mastodon/reducers/index.js4
-rw-r--r--app/javascript/mastodon/reducers/rules.js13
-rw-r--r--app/javascript/mastodon/reducers/server.js19
-rw-r--r--app/javascript/styles/mastodon/components.scss82
14 files changed, 249 insertions, 52 deletions
diff --git a/app/javascript/mastodon/actions/rules.js b/app/javascript/mastodon/actions/rules.js
deleted file mode 100644
index 34e60a121..000000000
--- a/app/javascript/mastodon/actions/rules.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import api from '../api';
-
-export const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST';
-export const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS';
-export const RULES_FETCH_FAIL    = 'RULES_FETCH_FAIL';
-
-export const fetchRules = () => (dispatch, getState) => {
-  dispatch(fetchRulesRequest());
-
-  api(getState)
-    .get('/api/v1/instance').then(({ data }) => dispatch(fetchRulesSuccess(data.rules)))
-    .catch(err => dispatch(fetchRulesFail(err)));
-};
-
-const fetchRulesRequest = () => ({
-  type: RULES_FETCH_REQUEST,
-});
-
-const fetchRulesSuccess = rules => ({
-  type: RULES_FETCH_SUCCESS,
-  rules,
-});
-
-const fetchRulesFail = error => ({
-  type: RULES_FETCH_FAIL,
-  error,
-});
diff --git a/app/javascript/mastodon/actions/server.js b/app/javascript/mastodon/actions/server.js
new file mode 100644
index 000000000..af8fef780
--- /dev/null
+++ b/app/javascript/mastodon/actions/server.js
@@ -0,0 +1,30 @@
+import api from '../api';
+import { importFetchedAccount } from './importer';
+
+export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST';
+export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS';
+export const SERVER_FETCH_FAIL    = 'Server_FETCH_FAIL';
+
+export const fetchServer = () => (dispatch, getState) => {
+  dispatch(fetchServerRequest());
+
+  api(getState)
+    .get('/api/v2/instance').then(({ data }) => {
+      if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
+      dispatch(fetchServerSuccess(data));
+    }).catch(err => dispatch(fetchServerFail(err)));
+};
+
+const fetchServerRequest = () => ({
+  type: SERVER_FETCH_REQUEST,
+});
+
+const fetchServerSuccess = server => ({
+  type: SERVER_FETCH_SUCCESS,
+  server,
+});
+
+const fetchServerFail = error => ({
+  type: SERVER_FETCH_FAIL,
+  error,
+});
diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js
index af9f119c8..36429e647 100644
--- a/app/javascript/mastodon/components/account.js
+++ b/app/javascript/mastodon/components/account.js
@@ -9,6 +9,7 @@ import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { me } from '../initial_state';
 import RelativeTimestamp from './relative_timestamp';
+import Skeleton from 'mastodon/components/skeleton';
 
 const messages = defineMessages({
   follow: { id: 'account.follow', defaultMessage: 'Follow' },
@@ -26,7 +27,7 @@ export default @injectIntl
 class Account extends ImmutablePureComponent {
 
   static propTypes = {
-    account: ImmutablePropTypes.map.isRequired,
+    account: ImmutablePropTypes.map,
     onFollow: PropTypes.func.isRequired,
     onBlock: PropTypes.func.isRequired,
     onMute: PropTypes.func.isRequired,
@@ -67,7 +68,16 @@ class Account extends ImmutablePureComponent {
     const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction } = this.props;
 
     if (!account) {
-      return <div />;
+      return (
+        <div className='account'>
+          <div className='account__wrapper'>
+            <div className='account__display-name'>
+              <div className='account__avatar-wrapper'><Skeleton width={36} height={36} /></div>
+              <DisplayName />
+            </div>
+          </div>
+        </div>
+      );
     }
 
     if (hidden) {
diff --git a/app/javascript/mastodon/components/display_name.js b/app/javascript/mastodon/components/display_name.js
index 7ccfbd0cd..e9139ab0f 100644
--- a/app/javascript/mastodon/components/display_name.js
+++ b/app/javascript/mastodon/components/display_name.js
@@ -2,11 +2,12 @@ import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import { autoPlayGif } from 'mastodon/initial_state';
+import Skeleton from 'mastodon/components/skeleton';
 
 export default class DisplayName extends React.PureComponent {
 
   static propTypes = {
-    account: ImmutablePropTypes.map.isRequired,
+    account: ImmutablePropTypes.map,
     others: ImmutablePropTypes.list,
     localDomain: PropTypes.string,
   };
@@ -48,7 +49,7 @@ export default class DisplayName extends React.PureComponent {
       if (others.size - 2 > 0) {
         suffix = `+${others.size - 2}`;
       }
-    } else {
+    } else if ((others && others.size > 0) || this.props.account) {
       if (others && others.size > 0) {
         account = others.first();
       } else {
@@ -63,6 +64,9 @@ export default class DisplayName extends React.PureComponent {
 
       displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
       suffix      = <span className='display-name__account'>@{acct}</span>;
+    } else {
+      displayName = <bdi><strong className='display-name__html'><Skeleton width='10ch' /></strong></bdi>;
+      suffix = <span className='display-name__account'><Skeleton width='7ch' /></span>;
     }
 
     return (
diff --git a/app/javascript/mastodon/components/server_banner.js b/app/javascript/mastodon/components/server_banner.js
new file mode 100644
index 000000000..bdd7f7380
--- /dev/null
+++ b/app/javascript/mastodon/components/server_banner.js
@@ -0,0 +1,91 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { domain } from 'mastodon/initial_state';
+import { fetchServer } from 'mastodon/actions/server';
+import { connect } from 'react-redux';
+import Account from 'mastodon/containers/account_container';
+import ShortNumber from 'mastodon/components/short_number';
+import Skeleton from 'mastodon/components/skeleton';
+import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
+
+const messages = defineMessages({
+  aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' },
+});
+
+const mapStateToProps = state => ({
+  server: state.get('server'),
+});
+
+export default @connect(mapStateToProps)
+@injectIntl
+class ServerBanner extends React.PureComponent {
+
+  static propTypes = {
+    server: PropTypes.object,
+    dispatch: PropTypes.func,
+    intl: PropTypes.object,
+  };
+
+  componentDidMount () {
+    const { dispatch } = this.props;
+    dispatch(fetchServer());
+  }
+
+  render () {
+    const { server, intl } = this.props;
+    const isLoading = server.get('isLoading');
+
+    return (
+      <div className='server-banner'>
+        <div className='server-banner__introduction'>
+          <FormattedMessage id='server_banner.introduction' defaultMessage='{domain} is part of the decentralized social network powered by {mastodon}.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} />
+        </div>
+
+        <img src={server.get('thumbnail')} alt={server.get('title')} className='server-banner__hero' />
+
+        <div className='server-banner__description'>
+          {isLoading ? (
+            <>
+              <Skeleton width='100%' />
+              <br />
+              <Skeleton width='100%' />
+              <br />
+              <Skeleton width='70%' />
+            </>
+          ) : server.get('description')}
+        </div>
+
+        <div className='server-banner__meta'>
+          <div className='server-banner__meta__column'>
+            <h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
+
+            <Account id={server.getIn(['contact', 'account', 'id'])} />
+          </div>
+
+          <div className='server-banner__meta__column'>
+            <h4><FormattedMessage id='server_banner.server_stats' defaultMessage='Server stats:' /></h4>
+
+            {isLoading ? (
+              <>
+                <strong className='server-banner__number'><Skeleton width='10ch' /></strong>
+                <br />
+                <span className='server-banner__number-label'><Skeleton width='5ch' /></span>
+              </>
+            ) : (
+              <>
+                <strong className='server-banner__number'><ShortNumber value={server.getIn(['usage', 'users', 'active_month'])} /></strong>
+                <br />
+                <span className='server-banner__number-label' title={intl.formatMessage(messages.aboutActiveUsers)}><FormattedMessage id='server_banner.active_users' defaultMessage='active users' /></span>
+              </>
+            )}
+          </div>
+        </div>
+
+        <hr className='spacer' />
+
+        <a className='button button--block button-secondary' href='/about/more' target='_blank'><FormattedMessage id='server_banner.learn_more' defaultMessage='Learn more' /></a>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/report/rules.js b/app/javascript/mastodon/features/report/rules.js
index f2db0d9e4..2cb4a95b5 100644
--- a/app/javascript/mastodon/features/report/rules.js
+++ b/app/javascript/mastodon/features/report/rules.js
@@ -7,7 +7,7 @@ import Button from 'mastodon/components/button';
 import Option from './components/option';
 
 const mapStateToProps = state => ({
-  rules: state.get('rules'),
+  rules: state.getIn(['server', 'rules']),
 });
 
 export default @connect(mapStateToProps)
diff --git a/app/javascript/mastodon/features/ui/components/compose_panel.js b/app/javascript/mastodon/features/ui/components/compose_panel.js
index 1c128188f..c8bc79a67 100644
--- a/app/javascript/mastodon/features/ui/components/compose_panel.js
+++ b/app/javascript/mastodon/features/ui/components/compose_panel.js
@@ -5,6 +5,7 @@ import SearchContainer from 'mastodon/features/compose/containers/search_contain
 import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
 import NavigationContainer from 'mastodon/features/compose/containers/navigation_container';
 import LinkFooter from './link_footer';
+import ServerBanner from 'mastodon/components/server_banner';
 import { changeComposing } from 'mastodon/actions/compose';
 
 export default @connect()
@@ -35,6 +36,7 @@ class ComposePanel extends React.PureComponent {
 
         {!signedIn && (
           <React.Fragment>
+            <ServerBanner />
             <div className='flex-spacer' />
           </React.Fragment>
         )}
diff --git a/app/javascript/mastodon/features/ui/components/report_modal.js b/app/javascript/mastodon/features/ui/components/report_modal.js
index 744dd248b..264da07ce 100644
--- a/app/javascript/mastodon/features/ui/components/report_modal.js
+++ b/app/javascript/mastodon/features/ui/components/report_modal.js
@@ -2,7 +2,7 @@ import React from 'react';
 import { connect } from 'react-redux';
 import { submitReport } from 'mastodon/actions/reports';
 import { expandAccountTimeline } from 'mastodon/actions/timelines';
-import { fetchRules } from 'mastodon/actions/rules';
+import { fetchServer } from 'mastodon/actions/server';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { makeGetAccount } from 'mastodon/selectors';
@@ -117,7 +117,7 @@ class ReportModal extends ImmutablePureComponent {
     const { dispatch, accountId } = this.props;
 
     dispatch(expandAccountTimeline(accountId, { withReplies: true }));
-    dispatch(fetchRules());
+    dispatch(fetchServer());
   }
 
   render () {
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 5825db1e4..efe460fab 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -13,7 +13,7 @@ import { debounce } from 'lodash';
 import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
 import { expandHomeTimeline } from '../../actions/timelines';
 import { expandNotifications } from '../../actions/notifications';
-import { fetchRules } from '../../actions/rules';
+import { fetchServer } from '../../actions/server';
 import { clearHeight } from '../../actions/height_cache';
 import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
 import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
@@ -389,7 +389,7 @@ class UI extends React.PureComponent {
       this.props.dispatch(expandHomeTimeline());
       this.props.dispatch(expandNotifications());
 
-      setTimeout(() => this.props.dispatch(fetchRules()), 3000);
+      setTimeout(() => this.props.dispatch(fetchServer()), 3000);
     }
 
     this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js
index 3587bb32a..08121005a 100644
--- a/app/javascript/mastodon/initial_state.js
+++ b/app/javascript/mastodon/initial_state.js
@@ -29,6 +29,5 @@ export const title = getMeta('title');
 export const cropImages = getMeta('crop_images');
 export const disableSwiping = getMeta('disable_swiping');
 export const languages = initialState && initialState.languages;
-export const server = initialState && initialState.server;
 
 export default initialState;
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index d3d0303df..bccdc1865 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -17,7 +17,7 @@ import status_lists from './status_lists';
 import mutes from './mutes';
 import blocks from './blocks';
 import boosts from './boosts';
-import rules from './rules';
+import server from './server';
 import contexts from './contexts';
 import compose from './compose';
 import search from './search';
@@ -62,7 +62,7 @@ const reducers = {
   mutes,
   blocks,
   boosts,
-  rules,
+  server,
   contexts,
   compose,
   search,
diff --git a/app/javascript/mastodon/reducers/rules.js b/app/javascript/mastodon/reducers/rules.js
deleted file mode 100644
index c1180b520..000000000
--- a/app/javascript/mastodon/reducers/rules.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { RULES_FETCH_SUCCESS } from 'mastodon/actions/rules';
-import { List as ImmutableList, fromJS } from 'immutable';
-
-const initialState = ImmutableList();
-
-export default function rules(state = initialState, action) {
-  switch (action.type) {
-  case RULES_FETCH_SUCCESS:
-    return fromJS(action.rules);
-  default:
-    return state;
-  }
-}
diff --git a/app/javascript/mastodon/reducers/server.js b/app/javascript/mastodon/reducers/server.js
new file mode 100644
index 000000000..68131c6dd
--- /dev/null
+++ b/app/javascript/mastodon/reducers/server.js
@@ -0,0 +1,19 @@
+import { SERVER_FETCH_REQUEST, SERVER_FETCH_SUCCESS, SERVER_FETCH_FAIL } from 'mastodon/actions/server';
+import { Map as ImmutableMap, fromJS } from 'immutable';
+
+const initialState = ImmutableMap({
+  isLoading: true,
+});
+
+export default function server(state = initialState, action) {
+  switch (action.type) {
+  case SERVER_FETCH_REQUEST:
+    return state.set('isLoading', true);
+  case SERVER_FETCH_SUCCESS:
+    return fromJS(action.server).set('isLoading', false);
+  case SERVER_FETCH_FAIL:
+    return state.set('isLoading', false);
+  default:
+    return state;
+  }
+}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index a75437b1c..b906117db 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -7949,3 +7949,85 @@ noscript {
     }
   }
 }
+
+.server-banner {
+  padding: 20px 0;
+
+  &__introduction {
+    color: $darker-text-color;
+    margin-bottom: 20px;
+
+    strong {
+      font-weight: 600;
+    }
+
+    a {
+      color: inherit;
+      text-decoration: underline;
+
+      &:hover,
+      &:active,
+      &:focus {
+        text-decoration: none;
+      }
+    }
+  }
+
+  &__hero {
+    display: block;
+    border-radius: 4px;
+    width: 100%;
+    height: auto;
+    margin-bottom: 20px;
+    aspect-ratio: 1.9;
+    border: 0;
+    background: $ui-base-color;
+    object-fit: cover;
+  }
+
+  &__description {
+    margin-bottom: 20px;
+  }
+
+  &__meta {
+    display: flex;
+    gap: 10px;
+    max-width: 100%;
+
+    &__column {
+      flex: 0 0 auto;
+      width: calc(50% - 5px);
+      overflow: hidden;
+    }
+  }
+
+  &__number {
+    font-weight: 600;
+    color: $primary-text-color;
+  }
+
+  &__number-label {
+    color: $darker-text-color;
+    font-weight: 500;
+  }
+
+  h4 {
+    text-transform: uppercase;
+    color: $darker-text-color;
+    margin-bottom: 10px;
+    font-weight: 600;
+  }
+
+  .account {
+    padding: 0;
+    border: 0;
+  }
+
+  .account__avatar-wrapper {
+    margin-left: 0;
+  }
+
+  .spacer {
+    margin: 10px 0;
+  }
+}