about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/components
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours/glitch/components')
-rw-r--r--app/javascript/flavours/glitch/components/autosuggest_hashtag.js24
-rw-r--r--app/javascript/flavours/glitch/components/common_counter.js62
-rw-r--r--app/javascript/flavours/glitch/components/hashtag.js49
-rw-r--r--app/javascript/flavours/glitch/components/short_number.js117
4 files changed, 242 insertions, 10 deletions
diff --git a/app/javascript/flavours/glitch/components/autosuggest_hashtag.js b/app/javascript/flavours/glitch/components/autosuggest_hashtag.js
index 648987dfd..d787ed07a 100644
--- a/app/javascript/flavours/glitch/components/autosuggest_hashtag.js
+++ b/app/javascript/flavours/glitch/components/autosuggest_hashtag.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { shortNumberFormat } from 'flavours/glitch/util/numbers';
+import ShortNumber from 'flavours/glitch/components/short_number';
 import { FormattedMessage } from 'react-intl';
 
 export default class AutosuggestHashtag extends React.PureComponent {
@@ -13,14 +13,28 @@ export default class AutosuggestHashtag extends React.PureComponent {
     }).isRequired,
   };
 
-  render () {
+  render() {
     const { tag } = this.props;
-    const weeklyUses = tag.history && shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0));
+    const weeklyUses = tag.history && (
+      <ShortNumber
+        value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
+      />
+    );
 
     return (
       <div className='autosuggest-hashtag'>
-        <div className='autosuggest-hashtag__name'>#<strong>{tag.name}</strong></div>
-        {tag.history !== undefined && <div className='autosuggest-hashtag__uses'><FormattedMessage id='autosuggest_hashtag.per_week' defaultMessage='{count} per week' values={{ count: weeklyUses }} /></div>}
+        <div className='autosuggest-hashtag__name'>
+          #<strong>{tag.name}</strong>
+        </div>
+        {tag.history !== undefined && (
+          <div className='autosuggest-hashtag__uses'>
+            <FormattedMessage
+              id='autosuggest_hashtag.per_week'
+              defaultMessage='{count} per week'
+              values={{ count: weeklyUses }}
+            />
+          </div>
+        )}
       </div>
     );
   }
diff --git a/app/javascript/flavours/glitch/components/common_counter.js b/app/javascript/flavours/glitch/components/common_counter.js
new file mode 100644
index 000000000..4fdf3babf
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/common_counter.js
@@ -0,0 +1,62 @@
+// @ts-check
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+
+/**
+ * Returns custom renderer for one of the common counter types
+ *
+ * @param {"statuses" | "following" | "followers"} counterType
+ * Type of the counter
+ * @param {boolean} isBold Whether display number must be displayed in bold
+ * @returns {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
+ * Renderer function
+ * @throws If counterType is not covered by this function
+ */
+export function counterRenderer(counterType, isBold = true) {
+  /**
+   * @type {(displayNumber: JSX.Element) => JSX.Element}
+   */
+  const renderCounter = isBold
+    ? (displayNumber) => <strong>{displayNumber}</strong>
+    : (displayNumber) => displayNumber;
+
+  switch (counterType) {
+  case 'statuses': {
+    return (displayNumber, pluralReady) => (
+      <FormattedMessage
+        id='account.statuses_counter'
+        defaultMessage='{count, plural, one {{counter} Toot} other {{counter} Toots}}'
+        values={{
+          count: pluralReady,
+          counter: renderCounter(displayNumber),
+        }}
+      />
+    );
+  }
+  case 'following': {
+    return (displayNumber, pluralReady) => (
+      <FormattedMessage
+        id='account.following_counter'
+        defaultMessage='{count, plural, other {{counter} Following}}'
+        values={{
+          count: pluralReady,
+          counter: renderCounter(displayNumber),
+        }}
+      />
+    );
+  }
+  case 'followers': {
+    return (displayNumber, pluralReady) => (
+      <FormattedMessage
+        id='account.followers_counter'
+        defaultMessage='{count, plural, one {{counter} Follower} other {{counter} Followers}}'
+        values={{
+          count: pluralReady,
+          counter: renderCounter(displayNumber),
+        }}
+      />
+    );
+  }
+  default: throw Error(`Incorrect counter name: ${counterType}. Ensure it accepted by commonCounter function`);
+  }
+}
diff --git a/app/javascript/flavours/glitch/components/hashtag.js b/app/javascript/flavours/glitch/components/hashtag.js
index d42bee0e9..639d87a1e 100644
--- a/app/javascript/flavours/glitch/components/hashtag.js
+++ b/app/javascript/flavours/glitch/components/hashtag.js
@@ -1,26 +1,65 @@
+// @ts-check
 import React from 'react';
 import { Sparklines, SparklinesCurve } from 'react-sparklines';
 import { FormattedMessage } from 'react-intl';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import Permalink from './permalink';
-import { shortNumberFormat } from 'flavours/glitch/util/numbers';
+import ShortNumber from 'flavours/glitch/components/short_number';
+
+/**
+ * Used to render counter of how much people are talking about hashtag
+ *
+ * @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
+ */
+const accountsCountRenderer = (displayNumber, pluralReady) => (
+  <FormattedMessage
+    id='trends.counter_by_accounts'
+    defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} talking'
+    values={{
+      count: pluralReady,
+      counter: <strong>{displayNumber}</strong>,
+    }}
+  />
+);
 
 const Hashtag = ({ hashtag }) => (
   <div className='trends__item'>
     <div className='trends__item__name'>
-      <Permalink href={hashtag.get('url')} to={`/timelines/tag/${hashtag.get('name')}`}>
+      <Permalink
+        href={hashtag.get('url')}
+        to={`/timelines/tag/${hashtag.get('name')}`}
+      >
         #<span>{hashtag.get('name')}</span>
       </Permalink>
 
-      <FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1, count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1)}</strong> }} />
+      <ShortNumber
+        value={
+          hashtag.getIn(['history', 0, 'accounts']) * 1 +
+          hashtag.getIn(['history', 1, 'accounts']) * 1
+        }
+        renderer={accountsCountRenderer}
+      />
     </div>
 
     <div className='trends__item__current'>
-      {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1)}
+      <ShortNumber
+        value={
+          hashtag.getIn(['history', 0, 'uses']) * 1 +
+          hashtag.getIn(['history', 1, 'uses']) * 1
+        }
+      />
     </div>
 
     <div className='trends__item__sparkline'>
-      <Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}>
+      <Sparklines
+        width={50}
+        height={28}
+        data={hashtag
+          .get('history')
+          .reverse()
+          .map((day) => day.get('uses'))
+          .toArray()}
+      >
         <SparklinesCurve style={{ fill: 'none' }} />
       </Sparklines>
     </div>
diff --git a/app/javascript/flavours/glitch/components/short_number.js b/app/javascript/flavours/glitch/components/short_number.js
new file mode 100644
index 000000000..e4ba09634
--- /dev/null
+++ b/app/javascript/flavours/glitch/components/short_number.js
@@ -0,0 +1,117 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../util/numbers';
+import { FormattedMessage, FormattedNumber } from 'react-intl';
+// @ts-check
+
+/**
+ * @callback ShortNumberRenderer
+ * @param {JSX.Element} displayNumber Number to display
+ * @param {number} pluralReady Number used for pluralization
+ * @returns {JSX.Element} Final render of number
+ */
+
+/**
+ * @typedef {object} ShortNumberProps
+ * @property {number} value Number to display in short variant
+ * @property {ShortNumberRenderer} [renderer]
+ * Custom renderer for numbers, provided as a prop. If another renderer
+ * passed as a child of this component, this prop won't be used.
+ * @property {ShortNumberRenderer} [children]
+ * Custom renderer for numbers, provided as a child. If another renderer
+ * passed as a prop of this component, this one will be used instead.
+ */
+
+/**
+ * Component that renders short big number to a shorter version
+ *
+ * @param {ShortNumberProps} param0 Props for the component
+ * @returns {JSX.Element} Rendered number
+ */
+function ShortNumber({ value, renderer, children }) {
+  const shortNumber = toShortNumber(value);
+  const [, division] = shortNumber;
+
+  // eslint-disable-next-line eqeqeq
+  if (children != null && renderer != null) {
+    console.warn('Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.');
+  }
+
+  // eslint-disable-next-line eqeqeq
+  const customRenderer = children != null ? children : renderer;
+
+  const displayNumber = <ShortNumberCounter value={shortNumber} />;
+
+  // eslint-disable-next-line eqeqeq
+  return customRenderer != null
+    ? customRenderer(displayNumber, pluralReady(value, division))
+    : displayNumber;
+}
+
+ShortNumber.propTypes = {
+  value: PropTypes.number.isRequired,
+  renderer: PropTypes.func,
+  children: PropTypes.func,
+};
+
+/**
+ * @typedef {object} ShortNumberCounterProps
+ * @property {import('../util/number').ShortNumber} value Short number
+ */
+
+/**
+ * Renders short number into corresponding localizable react fragment
+ *
+ * @param {ShortNumberCounterProps} param0 Props for the component
+ * @returns {JSX.Element} FormattedMessage ready to be embedded in code
+ */
+function ShortNumberCounter({ value }) {
+  const [rawNumber, unit, maxFractionDigits = 0] = value;
+
+  const count = (
+    <FormattedNumber
+      value={rawNumber}
+      maximumFractionDigits={maxFractionDigits}
+    />
+  );
+
+  let values = { count, rawNumber };
+
+  switch (unit) {
+  case DECIMAL_UNITS.THOUSAND: {
+    return (
+      <FormattedMessage
+        id='units.short.thousand'
+        defaultMessage='{count}K'
+        values={values}
+      />
+    );
+  }
+  case DECIMAL_UNITS.MILLION: {
+    return (
+      <FormattedMessage
+        id='units.short.million'
+        defaultMessage='{count}M'
+        values={values}
+      />
+    );
+  }
+  case DECIMAL_UNITS.BILLION: {
+    return (
+      <FormattedMessage
+        id='units.short.billion'
+        defaultMessage='{count}B'
+        values={values}
+      />
+    );
+  }
+  // Not sure if we should go farther - @Sasha-Sorokin
+  default: return count;
+  }
+}
+
+ShortNumberCounter.propTypes = {
+  value: PropTypes.arrayOf(PropTypes.number),
+};
+
+export default React.memo(ShortNumber);