Browse Source

Refactor icons in web UI to use Icon component (#9951)

* Refactor uses of icons to an Icon component in web UI

* Refactor options passed to the Icon component

* Make tests work with absolute component paths
Eugen Rochko 10 months ago
parent
commit
1f95190202
No account linked to committer's email address
38 changed files with 147 additions and 82 deletions
  1. 5
    0
      .eslintrc.js
  2. 3
    2
      app/javascript/mastodon/components/attachment_list.js
  3. 2
    1
      app/javascript/mastodon/components/column_back_button.js
  4. 2
    1
      app/javascript/mastodon/components/column_back_button_slim.js
  5. 8
    7
      app/javascript/mastodon/components/column_header.js
  6. 21
    0
      app/javascript/mastodon/components/icon.js
  7. 3
    2
      app/javascript/mastodon/components/icon_button.js
  8. 2
    1
      app/javascript/mastodon/components/load_gap.js
  9. 3
    2
      app/javascript/mastodon/components/status.js
  10. 2
    1
      app/javascript/mastodon/components/status_content.js
  11. 3
    2
      app/javascript/mastodon/features/account/components/header.js
  12. 2
    1
      app/javascript/mastodon/features/account_gallery/components/media_item.js
  13. 2
    1
      app/javascript/mastodon/features/account_timeline/components/moved_note.js
  14. 2
    1
      app/javascript/mastodon/features/compose/components/compose_form.js
  15. 2
    1
      app/javascript/mastodon/features/compose/components/privacy_dropdown.js
  16. 3
    2
      app/javascript/mastodon/features/compose/components/search.js
  17. 6
    5
      app/javascript/mastodon/features/compose/components/search_results.js
  18. 3
    2
      app/javascript/mastodon/features/compose/components/upload.js
  19. 2
    1
      app/javascript/mastodon/features/compose/components/upload_progress.js
  20. 8
    7
      app/javascript/mastodon/features/compose/index.js
  21. 2
    1
      app/javascript/mastodon/features/getting_started/index.js
  22. 2
    1
      app/javascript/mastodon/features/list_adder/components/list.js
  23. 3
    2
      app/javascript/mastodon/features/list_editor/components/search.js
  24. 3
    2
      app/javascript/mastodon/features/list_timeline/index.js
  25. 2
    1
      app/javascript/mastodon/features/notifications/components/clear_column_button.js
  26. 5
    4
      app/javascript/mastodon/features/notifications/components/filter_bar.js
  27. 4
    3
      app/javascript/mastodon/features/notifications/components/notification.js
  28. 4
    3
      app/javascript/mastodon/features/status/components/card.js
  29. 6
    5
      app/javascript/mastodon/features/status/components/detailed_status.js
  30. 2
    1
      app/javascript/mastodon/features/status/index.js
  31. 2
    1
      app/javascript/mastodon/features/ui/components/boost_modal.js
  32. 2
    1
      app/javascript/mastodon/features/ui/components/column_header.js
  33. 3
    2
      app/javascript/mastodon/features/ui/components/column_link.js
  34. 2
    1
      app/javascript/mastodon/features/ui/components/columns_area.js
  35. 3
    2
      app/javascript/mastodon/features/ui/components/media_modal.js
  36. 7
    6
      app/javascript/mastodon/features/ui/components/tabs_bar.js
  37. 7
    6
      app/javascript/mastodon/features/video/index.js
  38. 4
    0
      jest.config.js

+ 5
- 0
.eslintrc.js View File

@@ -41,6 +41,11 @@ module.exports = {
41 41
       'node_modules',
42 42
       '\\.(css|scss|json)$',
43 43
     ],
44
+    'import/resolver': {
45
+      node: {
46
+        paths: ['app/javascript'],
47
+      },
48
+    },
44 49
   },
45 50
 
46 51
   rules: {

+ 3
- 2
app/javascript/mastodon/components/attachment_list.js View File

@@ -2,6 +2,7 @@ import React from 'react';
2 2
 import ImmutablePropTypes from 'react-immutable-proptypes';
3 3
 import PropTypes from 'prop-types';
4 4
 import ImmutablePureComponent from 'react-immutable-pure-component';
5
+import Icon from 'mastodon/components/icon';
5 6
 
6 7
 const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
7 8
 
@@ -24,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent {
24 25
 
25 26
               return (
26 27
                 <li key={attachment.get('id')}>
27
-                  <a href={displayUrl} target='_blank' rel='noopener'><i className='fa fa-link' /> {filename(displayUrl)}</a>
28
+                  <a href={displayUrl} target='_blank' rel='noopener'><Icon id='link' /> {filename(displayUrl)}</a>
28 29
                 </li>
29 30
               );
30 31
             })}
@@ -36,7 +37,7 @@ export default class AttachmentList extends ImmutablePureComponent {
36 37
     return (
37 38
       <div className='attachment-list'>
38 39
         <div className='attachment-list__icon'>
39
-          <i className='fa fa-link' />
40
+          <Icon id='link' />
40 41
         </div>
41 42
 
42 43
         <ul className='attachment-list__list'>

+ 2
- 1
app/javascript/mastodon/components/column_back_button.js View File

@@ -1,6 +1,7 @@
1 1
 import React from 'react';
2 2
 import { FormattedMessage } from 'react-intl';
3 3
 import PropTypes from 'prop-types';
4
+import Icon from 'mastodon/components/icon';
4 5
 
5 6
 export default class ColumnBackButton extends React.PureComponent {
6 7
 
@@ -19,7 +20,7 @@ export default class ColumnBackButton extends React.PureComponent {
19 20
   render () {
20 21
     return (
21 22
       <button onClick={this.handleClick} className='column-back-button'>
22
-        <i className='fa fa-fw fa-chevron-left column-back-button__icon' />
23
+        <Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
23 24
         <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
24 25
       </button>
25 26
     );

+ 2
- 1
app/javascript/mastodon/components/column_back_button_slim.js View File

@@ -1,6 +1,7 @@
1 1
 import React from 'react';
2 2
 import { FormattedMessage } from 'react-intl';
3 3
 import ColumnBackButton from './column_back_button';
4
+import Icon from 'mastodon/components/icon';
4 5
 
5 6
 export default class ColumnBackButtonSlim extends ColumnBackButton {
6 7
 
@@ -8,7 +9,7 @@ export default class ColumnBackButtonSlim extends ColumnBackButton {
8 9
     return (
9 10
       <div className='column-back-button--slim'>
10 11
         <div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
11
-          <i className='fa fa-fw fa-chevron-left column-back-button__icon' />
12
+          <Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
12 13
           <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
13 14
         </div>
14 15
       </div>

+ 8
- 7
app/javascript/mastodon/components/column_header.js View File

@@ -2,6 +2,7 @@ import React from 'react';
2 2
 import PropTypes from 'prop-types';
3 3
 import classNames from 'classnames';
4 4
 import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
5
+import Icon from 'mastodon/components/icon';
5 6
 
6 7
 const messages = defineMessages({
7 8
   show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
@@ -109,22 +110,22 @@ class ColumnHeader extends React.PureComponent {
109 110
     }
110 111
 
111 112
     if (multiColumn && pinned) {
112
-      pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
113
+      pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
113 114
 
114 115
       moveButtons = (
115 116
         <div key='move-buttons' className='column-header__setting-arrows'>
116
-          <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button>
117
-          <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button>
117
+          <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button>
118
+          <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
118 119
         </div>
119 120
       );
120 121
     } else if (multiColumn) {
121
-      pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
122
+      pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
122 123
     }
123 124
 
124 125
     if (!pinned && (multiColumn || showBackButton)) {
125 126
       backButton = (
126 127
         <button onClick={this.handleBackClick} className='column-header__back-button'>
127
-          <i className='fa fa-fw fa-chevron-left column-back-button__icon' />
128
+          <Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
128 129
           <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
129 130
         </button>
130 131
       );
@@ -140,7 +141,7 @@ class ColumnHeader extends React.PureComponent {
140 141
     }
141 142
 
142 143
     if (children || multiColumn) {
143
-      collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>;
144
+      collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>;
144 145
     }
145 146
 
146 147
     const hasTitle = icon && title;
@@ -150,7 +151,7 @@ class ColumnHeader extends React.PureComponent {
150 151
         <h1 className={buttonClassName}>
151 152
           {hasTitle && (
152 153
             <button onClick={this.handleTitleClick}>
153
-              <i className={`fa fa-fw fa-${icon} column-header__icon`} />
154
+              <Icon id={icon} fixedWidth className='column-header__icon' />
154 155
               {title}
155 156
             </button>
156 157
           )}

+ 21
- 0
app/javascript/mastodon/components/icon.js View File

@@ -0,0 +1,21 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+import classNames from 'classnames';
4
+
5
+export default class Icon extends React.PureComponent {
6
+
7
+  static propTypes = {
8
+    id: PropTypes.string.isRequired,
9
+    className: PropTypes.string,
10
+    fixedWidth: PropTypes.bool,
11
+  };
12
+
13
+  render () {
14
+    const { id, className, fixedWidth, ...other } = this.props;
15
+
16
+    return (
17
+      <i role='img' className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />
18
+    );
19
+  }
20
+
21
+}

+ 3
- 2
app/javascript/mastodon/components/icon_button.js View File

@@ -3,6 +3,7 @@ import Motion from '../features/ui/util/optional_motion';
3 3
 import spring from 'react-motion/lib/spring';
4 4
 import PropTypes from 'prop-types';
5 5
 import classNames from 'classnames';
6
+import Icon from 'mastodon/components/icon';
6 7
 
7 8
 export default class IconButton extends React.PureComponent {
8 9
 
@@ -86,7 +87,7 @@ export default class IconButton extends React.PureComponent {
86 87
           style={style}
87 88
           tabIndex={tabIndex}
88 89
         >
89
-          <i className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
90
+          <Icon id={icon} fixedWidth aria-hidden='true' />
90 91
         </button>
91 92
       );
92 93
     }
@@ -104,7 +105,7 @@ export default class IconButton extends React.PureComponent {
104 105
             style={style}
105 106
             tabIndex={tabIndex}
106 107
           >
107
-            <i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
108
+            <Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />
108 109
           </button>
109 110
         )}
110 111
       </Motion>

+ 2
- 1
app/javascript/mastodon/components/load_gap.js View File

@@ -1,6 +1,7 @@
1 1
 import React from 'react';
2 2
 import PropTypes from 'prop-types';
3 3
 import { injectIntl, defineMessages } from 'react-intl';
4
+import Icon from 'mastodon/components/icon';
4 5
 
5 6
 const messages = defineMessages({
6 7
   load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
@@ -25,7 +26,7 @@ class LoadGap extends React.PureComponent {
25 26
 
26 27
     return (
27 28
       <button className='load-more load-gap' disabled={disabled} onClick={this.handleClick} aria-label={intl.formatMessage(messages.load_more)}>
28
-        <i className='fa fa-ellipsis-h' />
29
+        <Icon id='ellipsis-h' />
29 30
       </button>
30 31
     );
31 32
   }

+ 3
- 2
app/javascript/mastodon/components/status.js View File

@@ -15,6 +15,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
15 15
 import { MediaGallery, Video } from '../features/ui/util/async-components';
16 16
 import { HotKeys } from 'react-hotkeys';
17 17
 import classNames from 'classnames';
18
+import Icon from 'mastodon/components/icon';
18 19
 
19 20
 // We use the component (and not the container) since we do not want
20 21
 // to use the progress bar to show download progress
@@ -204,7 +205,7 @@ class Status extends ImmutablePureComponent {
204 205
     if (featured) {
205 206
       prepend = (
206 207
         <div className='status__prepend'>
207
-          <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-thumb-tack status__prepend-icon' /></div>
208
+          <div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' className='status__prepend-icon' fixedWidth /></div>
208 209
           <FormattedMessage id='status.pinned' defaultMessage='Pinned toot' />
209 210
         </div>
210 211
       );
@@ -213,7 +214,7 @@ class Status extends ImmutablePureComponent {
213 214
 
214 215
       prepend = (
215 216
         <div className='status__prepend'>
216
-          <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
217
+          <div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></div>
217 218
           <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
218 219
         </div>
219 220
       );

+ 2
- 1
app/javascript/mastodon/components/status_content.js View File

@@ -5,6 +5,7 @@ import { isRtl } from '../rtl';
5 5
 import { FormattedMessage } from 'react-intl';
6 6
 import Permalink from './permalink';
7 7
 import classnames from 'classnames';
8
+import Icon from 'mastodon/components/icon';
8 9
 
9 10
 const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
10 11
 
@@ -160,7 +161,7 @@ export default class StatusContent extends React.PureComponent {
160 161
 
161 162
     const readMoreButton = (
162 163
       <button className='status__content__read-more-button' onClick={this.props.onClick} key='read-more'>
163
-        <FormattedMessage id='status.read_more' defaultMessage='Read more' /><i className='fa fa-fw fa-angle-right' />
164
+        <FormattedMessage id='status.read_more' defaultMessage='Read more' /><Icon id='angle-right' fixedWidth />
164 165
       </button>
165 166
     );
166 167
 

+ 3
- 2
app/javascript/mastodon/features/account/components/header.js View File

@@ -8,6 +8,7 @@ import spring from 'react-motion/lib/spring';
8 8
 import ImmutablePureComponent from 'react-immutable-pure-component';
9 9
 import { autoPlayGif, me } from '../../../initial_state';
10 10
 import classNames from 'classnames';
11
+import Icon from 'mastodon/components/icon';
11 12
 
12 13
 const messages = defineMessages({
13 14
   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@@ -149,7 +150,7 @@ class Header extends ImmutablePureComponent {
149 150
     }
150 151
 
151 152
     if (account.get('locked')) {
152
-      lockedIcon = <i className='fa fa-lock' title={intl.formatMessage(messages.account_locked)} />;
153
+      lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />;
153 154
     }
154 155
 
155 156
     const content         = { __html: account.get('note_emojified') };
@@ -176,7 +177,7 @@ class Header extends ImmutablePureComponent {
176 177
                   <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
177 178
 
178 179
                   <dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}>
179
-                    {pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><i className='fa fa-check verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
180
+                    {pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
180 181
                   </dd>
181 182
                 </dl>
182 183
               ))}

+ 2
- 1
app/javascript/mastodon/features/account_gallery/components/media_item.js View File

@@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
3 3
 import ImmutablePureComponent from 'react-immutable-pure-component';
4 4
 import Permalink from '../../../components/permalink';
5 5
 import { displayMedia } from '../../../initial_state';
6
+import Icon from 'mastodon/components/icon';
6 7
 
7 8
 export default class MediaItem extends ImmutablePureComponent {
8 9
 
@@ -45,7 +46,7 @@ export default class MediaItem extends ImmutablePureComponent {
45 46
     } else {
46 47
       icon = (
47 48
         <span className='account-gallery__item__icons'>
48
-          <i className='fa fa-eye-slash' />
49
+          <Icon id='eye-slash' />
49 50
         </span>
50 51
       );
51 52
     }

+ 2
- 1
app/javascript/mastodon/features/account_timeline/components/moved_note.js View File

@@ -5,6 +5,7 @@ import { FormattedMessage } from 'react-intl';
5 5
 import ImmutablePureComponent from 'react-immutable-pure-component';
6 6
 import AvatarOverlay from '../../../components/avatar_overlay';
7 7
 import DisplayName from '../../../components/display_name';
8
+import Icon from 'mastodon/components/icon';
8 9
 
9 10
 export default class MovedNote extends ImmutablePureComponent {
10 11
 
@@ -33,7 +34,7 @@ export default class MovedNote extends ImmutablePureComponent {
33 34
     return (
34 35
       <div className='account__moved-note'>
35 36
         <div className='account__moved-note__message'>
36
-          <div className='account__moved-note__icon-wrapper'><i className='fa fa-fw fa-suitcase account__moved-note__icon' /></div>
37
+          <div className='account__moved-note__icon-wrapper'><Icon id='suitcase' className='account__moved-note__icon' fixedWidth /></div>
37 38
           <FormattedMessage id='account.moved_to' defaultMessage='{name} has moved to:' values={{ name: <bdi><strong dangerouslySetInnerHTML={displayNameHtml} /></bdi> }} />
38 39
         </div>
39 40
 

+ 2
- 1
app/javascript/mastodon/features/compose/components/compose_form.js View File

@@ -17,6 +17,7 @@ import { isMobile } from '../../../is_mobile';
17 17
 import ImmutablePureComponent from 'react-immutable-pure-component';
18 18
 import { length } from 'stringz';
19 19
 import { countableText } from '../util/counter';
20
+import Icon from 'mastodon/components/icon';
20 21
 
21 22
 const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
22 23
 
@@ -165,7 +166,7 @@ class ComposeForm extends ImmutablePureComponent {
165 166
     let publishText = '';
166 167
 
167 168
     if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
168
-      publishText = <span className='compose-form__publish-private'><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
169
+      publishText = <span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span>;
169 170
     } else {
170 171
       publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
171 172
     }

+ 2
- 1
app/javascript/mastodon/features/compose/components/privacy_dropdown.js View File

@@ -7,6 +7,7 @@ import Motion from '../../ui/util/optional_motion';
7 7
 import spring from 'react-motion/lib/spring';
8 8
 import detectPassiveEvents from 'detect-passive-events';
9 9
 import classNames from 'classnames';
10
+import Icon from 'mastodon/components/icon';
10 11
 
11 12
 const messages = defineMessages({
12 13
   public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
@@ -132,7 +133,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
132 133
             {items.map(item => (
133 134
               <div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
134 135
                 <div className='privacy-dropdown__option__icon'>
135
-                  <i className={`fa fa-fw fa-${item.icon}`} />
136
+                  <Icon id={item.icon} fixedWidth />
136 137
                 </div>
137 138
 
138 139
                 <div className='privacy-dropdown__option__content'>

+ 3
- 2
app/javascript/mastodon/features/compose/components/search.js View File

@@ -5,6 +5,7 @@ import Overlay from 'react-overlays/lib/Overlay';
5 5
 import Motion from '../../ui/util/optional_motion';
6 6
 import spring from 'react-motion/lib/spring';
7 7
 import { searchEnabled } from '../../../initial_state';
8
+import Icon from 'mastodon/components/icon';
8 9
 
9 10
 const messages = defineMessages({
10 11
   placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
@@ -116,8 +117,8 @@ class Search extends React.PureComponent {
116 117
         </label>
117 118
 
118 119
         <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
119
-          <i className={`fa fa-search ${hasValue ? '' : 'active'}`} />
120
-          <i aria-label={intl.formatMessage(messages.placeholder)} className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} />
120
+          <Icon id='search' className={hasValue ? '' : 'active'} />
121
+          <Icon id='times-circle' className={hasValue ? 'active' : ''} aria-label={intl.formatMessage(messages.placeholder)} />
121 122
         </div>
122 123
 
123 124
         <Overlay show={expanded && !hasValue} placement='bottom' target={this}>

+ 6
- 5
app/javascript/mastodon/features/compose/components/search_results.js View File

@@ -6,6 +6,7 @@ import AccountContainer from '../../../containers/account_container';
6 6
 import StatusContainer from '../../../containers/status_container';
7 7
 import ImmutablePureComponent from 'react-immutable-pure-component';
8 8
 import Hashtag from '../../../components/hashtag';
9
+import Icon from 'mastodon/components/icon';
9 10
 
10 11
 const messages = defineMessages({
11 12
   dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
@@ -34,7 +35,7 @@ class SearchResults extends ImmutablePureComponent {
34 35
         <div className='search-results'>
35 36
           <div className='trends'>
36 37
             <div className='trends__header'>
37
-              <i className='fa fa-user-plus fa-fw' />
38
+              <Icon id='user-plus' fixedWidth />
38 39
               <FormattedMessage id='suggestions.header' defaultMessage='You might be interested in…' />
39 40
             </div>
40 41
 
@@ -59,7 +60,7 @@ class SearchResults extends ImmutablePureComponent {
59 60
       count   += results.get('accounts').size;
60 61
       accounts = (
61 62
         <div className='search-results__section'>
62
-          <h5><i className='fa fa-fw fa-users' /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>
63
+          <h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>
63 64
 
64 65
           {results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
65 66
         </div>
@@ -70,7 +71,7 @@ class SearchResults extends ImmutablePureComponent {
70 71
       count   += results.get('statuses').size;
71 72
       statuses = (
72 73
         <div className='search-results__section'>
73
-          <h5><i className='fa fa-fw fa-quote-right' /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
74
+          <h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
74 75
 
75 76
           {results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
76 77
         </div>
@@ -81,7 +82,7 @@ class SearchResults extends ImmutablePureComponent {
81 82
       count += results.get('hashtags').size;
82 83
       hashtags = (
83 84
         <div className='search-results__section'>
84
-          <h5><i className='fa fa-fw fa-hashtag' /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
85
+          <h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
85 86
 
86 87
           {results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
87 88
         </div>
@@ -91,7 +92,7 @@ class SearchResults extends ImmutablePureComponent {
91 92
     return (
92 93
       <div className='search-results'>
93 94
         <div className='search-results__header'>
94
-          <i className='fa fa-search fa-fw' />
95
+          <Icon id='search' fixedWidth />
95 96
           <FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} />
96 97
         </div>
97 98
 

+ 3
- 2
app/javascript/mastodon/features/compose/components/upload.js View File

@@ -6,6 +6,7 @@ import spring from 'react-motion/lib/spring';
6 6
 import ImmutablePureComponent from 'react-immutable-pure-component';
7 7
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
8 8
 import classNames from 'classnames';
9
+import Icon from 'mastodon/components/icon';
9 10
 
10 11
 const messages = defineMessages({
11 12
   description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
@@ -99,8 +100,8 @@ class Upload extends ImmutablePureComponent {
99 100
           {({ scale }) => (
100 101
             <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
101 102
               <div className={classNames('compose-form__upload__actions', { active })}>
102
-                <button className='icon-button' onClick={this.handleUndoClick}><i className='fa fa-times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
103
-                {media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><i className='fa fa-crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>}
103
+                <button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
104
+                {media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>}
104 105
               </div>
105 106
 
106 107
               <div className={classNames('compose-form__upload-description', { active })}>

+ 2
- 1
app/javascript/mastodon/features/compose/components/upload_progress.js View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
3 3
 import Motion from '../../ui/util/optional_motion';
4 4
 import spring from 'react-motion/lib/spring';
5 5
 import { FormattedMessage } from 'react-intl';
6
+import Icon from 'mastodon/components/icon';
6 7
 
7 8
 export default class UploadProgress extends React.PureComponent {
8 9
 
@@ -21,7 +22,7 @@ export default class UploadProgress extends React.PureComponent {
21 22
     return (
22 23
       <div className='upload-progress'>
23 24
         <div className='upload-progress__icon'>
24
-          <i className='fa fa-upload' />
25
+          <Icon id='upload' />
25 26
         </div>
26 27
 
27 28
         <div className='upload-progress__message'>

+ 8
- 7
app/javascript/mastodon/features/compose/index.js View File

@@ -14,6 +14,7 @@ import SearchResultsContainer from './containers/search_results_container';
14 14
 import { changeComposing } from '../../actions/compose';
15 15
 import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
16 16
 import { mascot } from '../../initial_state';
17
+import Icon from 'mastodon/components/icon';
17 18
 
18 19
 const messages = defineMessages({
19 20
   start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@@ -77,21 +78,21 @@ class Compose extends React.PureComponent {
77 78
       const { columns } = this.props;
78 79
       header = (
79 80
         <nav className='drawer__header'>
80
-          <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><i role='img' className='fa fa-fw fa-bars' /></Link>
81
+          <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><Icon id='bars' fixedWidth /></Link>
81 82
           {!columns.some(column => column.get('id') === 'HOME') && (
82
-            <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><i role='img' className='fa fa-fw fa-home' /></Link>
83
+            <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' fixedWidth /></Link>
83 84
           )}
84 85
           {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
85
-            <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><i role='img' className='fa fa-fw fa-bell' /></Link>
86
+            <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' fixedWidth /></Link>
86 87
           )}
87 88
           {!columns.some(column => column.get('id') === 'COMMUNITY') && (
88
-            <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><i role='img' className='fa fa-fw fa-users' /></Link>
89
+            <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link>
89 90
           )}
90 91
           {!columns.some(column => column.get('id') === 'PUBLIC') && (
91
-            <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><i role='img' className='fa fa-fw fa-globe' /></Link>
92
+            <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link>
92 93
           )}
93
-          <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><i role='img' className='fa fa-fw fa-cog' /></a>
94
-          <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><i role='img' className='fa fa-fw fa-sign-out' /></a>
94
+          <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a>
95
+          <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><Icon id='sign-out' fixedWidth /></a>
95 96
         </nav>
96 97
       );
97 98
     }

+ 2
- 1
app/javascript/mastodon/features/getting_started/index.js View File

@@ -12,6 +12,7 @@ import { fetchFollowRequests } from '../../actions/accounts';
12 12
 import { List as ImmutableList } from 'immutable';
13 13
 import { Link } from 'react-router-dom';
14 14
 import NavigationBar from '../compose/components/navigation_bar';
15
+import Icon from 'mastodon/components/icon';
15 16
 
16 17
 const messages = defineMessages({
17 18
   home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
@@ -140,7 +141,7 @@ class GettingStarted extends ImmutablePureComponent {
140 141
         {multiColumn && <div className='column-header__wrapper'>
141 142
           <h1 className='column-header'>
142 143
             <button>
143
-              <i className='fa fa-bars fa-fw column-header__icon' />
144
+              <Icon id='bars' className='column-header__icon' fixedWidth />
144 145
               <FormattedMessage id='getting_started.heading' defaultMessage='Getting started' />
145 146
             </button>
146 147
           </h1>

+ 2
- 1
app/javascript/mastodon/features/list_adder/components/list.js View File

@@ -6,6 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
6 6
 import IconButton from '../../../components/icon_button';
7 7
 import { defineMessages, injectIntl } from 'react-intl';
8 8
 import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
9
+import Icon from 'mastodon/components/icon';
9 10
 
10 11
 const messages = defineMessages({
11 12
   remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
@@ -53,7 +54,7 @@ class List extends ImmutablePureComponent {
53 54
       <div className='list'>
54 55
         <div className='list__wrapper'>
55 56
           <div className='list__display-name'>
56
-            <i className='fa fa-fw fa-list-ul column-link__icon' />
57
+            <Icon id='list-ul' className='column-link__icon' fixedWidth />
57 58
             {list.get('title')}
58 59
           </div>
59 60
 

+ 3
- 2
app/javascript/mastodon/features/list_editor/components/search.js View File

@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
4 4
 import { defineMessages, injectIntl } from 'react-intl';
5 5
 import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
6 6
 import classNames from 'classnames';
7
+import Icon from 'mastodon/components/icon';
7 8
 
8 9
 const messages = defineMessages({
9 10
   search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
@@ -65,8 +66,8 @@ class Search extends React.PureComponent {
65 66
         </label>
66 67
 
67 68
         <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
68
-          <i className={classNames('fa fa-search', { active: !hasValue })} />
69
-          <i aria-label={intl.formatMessage(messages.search)} className={classNames('fa fa-times-circle', { active: hasValue })} />
69
+          <Icon id='search' className={classNames({ active: !hasValue })} />
70
+          <Icon id='times-circle' aria-label={intl.formatMessage(messages.search)} className={classNames({ active: hasValue })} />
70 71
         </div>
71 72
       </div>
72 73
     );

+ 3
- 2
app/javascript/mastodon/features/list_timeline/index.js View File

@@ -14,6 +14,7 @@ import { fetchList, deleteList } from '../../actions/lists';
14 14
 import { openModal } from '../../actions/modal';
15 15
 import MissingIndicator from '../../components/missing_indicator';
16 16
 import LoadingIndicator from '../../components/loading_indicator';
17
+import Icon from 'mastodon/components/icon';
17 18
 
18 19
 const messages = defineMessages({
19 20
   deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
@@ -150,11 +151,11 @@ class ListTimeline extends React.PureComponent {
150 151
         >
151 152
           <div className='column-header__links'>
152 153
             <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}>
153
-              <i className='fa fa-pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
154
+              <Icon id='pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
154 155
             </button>
155 156
 
156 157
             <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleDeleteClick}>
157
-              <i className='fa fa-trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
158
+              <Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
158 159
             </button>
159 160
           </div>
160 161
 

+ 2
- 1
app/javascript/mastodon/features/notifications/components/clear_column_button.js View File

@@ -1,6 +1,7 @@
1 1
 import React from 'react';
2 2
 import PropTypes from 'prop-types';
3 3
 import { FormattedMessage } from 'react-intl';
4
+import Icon from 'mastodon/components/icon';
4 5
 
5 6
 export default class ClearColumnButton extends React.PureComponent {
6 7
 
@@ -10,7 +11,7 @@ export default class ClearColumnButton extends React.PureComponent {
10 11
 
11 12
   render () {
12 13
     return (
13
-      <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.props.onClick}><i className='fa fa-eraser' /> <FormattedMessage id='notifications.clear' defaultMessage='Clear notifications' /></button>
14
+      <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.props.onClick}><Icon id='eraser' /> <FormattedMessage id='notifications.clear' defaultMessage='Clear notifications' /></button>
14 15
     );
15 16
   }
16 17
 

+ 5
- 4
app/javascript/mastodon/features/notifications/components/filter_bar.js View File

@@ -1,6 +1,7 @@
1 1
 import React from 'react';
2 2
 import PropTypes from 'prop-types';
3 3
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
4
+import Icon from 'mastodon/components/icon';
4 5
 
5 6
 const tooltips = defineMessages({
6 7
   mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
@@ -62,28 +63,28 @@ class FilterBar extends React.PureComponent {
62 63
           onClick={this.onClick('mention')}
63 64
           title={intl.formatMessage(tooltips.mentions)}
64 65
         >
65
-          <i className='fa fa-fw fa-at' />
66
+          <Icon id='at' fixedWidth />
66 67
         </button>
67 68
         <button
68 69
           className={selectedFilter === 'favourite' ? 'active' : ''}
69 70
           onClick={this.onClick('favourite')}
70 71
           title={intl.formatMessage(tooltips.favourites)}
71 72
         >
72
-          <i className='fa fa-fw fa-star' />
73
+          <Icon id='star' fixedWidth />
73 74
         </button>
74 75
         <button
75 76
           className={selectedFilter === 'reblog' ? 'active' : ''}
76 77
           onClick={this.onClick('reblog')}
77 78
           title={intl.formatMessage(tooltips.boosts)}
78 79
         >
79
-          <i className='fa fa-fw fa-retweet' />
80
+          <Icon id='retweet' fixedWidth />
80 81
         </button>
81 82
         <button
82 83
           className={selectedFilter === 'follow' ? 'active' : ''}
83 84
           onClick={this.onClick('follow')}
84 85
           title={intl.formatMessage(tooltips.follows)}
85 86
         >
86
-          <i className='fa fa-fw fa-user-plus' />
87
+          <Icon id='user-plus' fixedWidth />
87 88
         </button>
88 89
       </div>
89 90
     );

+ 4
- 3
app/javascript/mastodon/features/notifications/components/notification.js View File

@@ -7,6 +7,7 @@ import { injectIntl, FormattedMessage } from 'react-intl';
7 7
 import Permalink from '../../../components/permalink';
8 8
 import ImmutablePureComponent from 'react-immutable-pure-component';
9 9
 import { HotKeys } from 'react-hotkeys';
10
+import Icon from 'mastodon/components/icon';
10 11
 
11 12
 const notificationForScreenReader = (intl, message, timestamp) => {
12 13
   const output = [message];
@@ -105,7 +106,7 @@ class Notification extends ImmutablePureComponent {
105 106
         <div className='notification notification-follow focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow', defaultMessage: '{name} followed you' }, { name: account.get('acct') }), notification.get('created_at'))}>
106 107
           <div className='notification__message'>
107 108
             <div className='notification__favourite-icon-wrapper'>
108
-              <i className='fa fa-fw fa-user-plus' />
109
+              <Icon id='user-plus' fixedWidth />
109 110
             </div>
110 111
 
111 112
             <span title={notification.get('created_at')}>
@@ -140,7 +141,7 @@ class Notification extends ImmutablePureComponent {
140 141
         <div className='notification notification-favourite focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.favourite', defaultMessage: '{name} favourited your status' }, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
141 142
           <div className='notification__message'>
142 143
             <div className='notification__favourite-icon-wrapper'>
143
-              <i className='fa fa-fw fa-star star-icon' />
144
+              <Icon id='star' className='star-icon' fixedWidth />
144 145
             </div>
145 146
 
146 147
             <span title={notification.get('created_at')}>
@@ -162,7 +163,7 @@ class Notification extends ImmutablePureComponent {
162 163
         <div className='notification notification-reblog focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.reblog', defaultMessage: '{name} boosted your status' }, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
163 164
           <div className='notification__message'>
164 165
             <div className='notification__favourite-icon-wrapper'>
165
-              <i className='fa fa-fw fa-retweet' />
166
+              <Icon id='retweet' fixedWidth />
166 167
             </div>
167 168
 
168 169
             <span title={notification.get('created_at')}>

+ 4
- 3
app/javascript/mastodon/features/status/components/card.js View File

@@ -4,6 +4,7 @@ import Immutable from 'immutable';
4 4
 import ImmutablePropTypes from 'react-immutable-proptypes';
5 5
 import punycode from 'punycode';
6 6
 import classnames from 'classnames';
7
+import Icon from 'mastodon/components/icon';
7 8
 
8 9
 const IDNA_PREFIX = 'xn--';
9 10
 
@@ -175,8 +176,8 @@ export default class Card extends React.PureComponent {
175 176
 
176 177
             <div className='status-card__actions'>
177 178
               <div>
178
-                <button onClick={this.handleEmbedClick}><i className={`fa fa-${iconVariant}`} /></button>
179
-                {horizontal && <a href={card.get('url')} target='_blank' rel='noopener'><i className='fa fa-external-link' /></a>}
179
+                <button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
180
+                {horizontal && <a href={card.get('url')} target='_blank' rel='noopener'><Icon id='external-link' /></a>}
180 181
               </div>
181 182
             </div>
182 183
           </div>
@@ -198,7 +199,7 @@ export default class Card extends React.PureComponent {
198 199
     } else {
199 200
       embed = (
200 201
         <div className='status-card__image'>
201
-          <i className='fa fa-file-text' />
202
+          <Icon id='file-text' />
202 203
         </div>
203 204
       );
204 205
     }

+ 6
- 5
app/javascript/mastodon/features/status/components/detailed_status.js View File

@@ -13,6 +13,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
13 13
 import Video from '../../video';
14 14
 import scheduleIdleTask from '../../ui/util/schedule_idle_task';
15 15
 import classNames from 'classnames';
16
+import Icon from 'mastodon/components/icon';
16 17
 
17 18
 export default class DetailedStatus extends ImmutablePureComponent {
18 19
 
@@ -148,11 +149,11 @@ export default class DetailedStatus extends ImmutablePureComponent {
148 149
     }
149 150
 
150 151
     if (status.get('visibility') === 'private') {
151
-      reblogLink = <i className={`fa fa-${reblogIcon}`} />;
152
+      reblogLink = <Icon id={reblogIcon} />;
152 153
     } else if (this.context.router) {
153 154
       reblogLink = (
154 155
         <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
155
-          <i className={`fa fa-${reblogIcon}`} />
156
+          <Icon id={reblogIcon} />
156 157
           <span className='detailed-status__reblogs'>
157 158
             <FormattedNumber value={status.get('reblogs_count')} />
158 159
           </span>
@@ -161,7 +162,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
161 162
     } else {
162 163
       reblogLink = (
163 164
         <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
164
-          <i className={`fa fa-${reblogIcon}`} />
165
+          <Icon id={reblogIcon} />
165 166
           <span className='detailed-status__reblogs'>
166 167
             <FormattedNumber value={status.get('reblogs_count')} />
167 168
           </span>
@@ -172,7 +173,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
172 173
     if (this.context.router) {
173 174
       favouriteLink = (
174 175
         <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
175
-          <i className='fa fa-star' />
176
+          <Icon id='star' />
176 177
           <span className='detailed-status__favorites'>
177 178
             <FormattedNumber value={status.get('favourites_count')} />
178 179
           </span>
@@ -181,7 +182,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
181 182
     } else {
182 183
       favouriteLink = (
183 184
         <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
184
-          <i className='fa fa-star' />
185
+          <Icon id='star' />
185 186
           <span className='detailed-status__favorites'>
186 187
             <FormattedNumber value={status.get('favourites_count')} />
187 188
           </span>

+ 2
- 1
app/javascript/mastodon/features/status/index.js View File

@@ -44,6 +44,7 @@ import { HotKeys } from 'react-hotkeys';
44 44
 import { boostModal, deleteModal } from '../../initial_state';
45 45
 import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
46 46
 import { textForScreenReader } from '../../components/status';
47
+import Icon from 'mastodon/components/icon';
47 48
 
48 49
 const messages = defineMessages({
49 50
   deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@@ -425,7 +426,7 @@ class Status extends ImmutablePureComponent {
425 426
         <ColumnHeader
426 427
           showBackButton
427 428
           extraButton={(
428
-            <button className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={status.get('hidden') ? 'false' : 'true'}><i className={`fa fa-${status.get('hidden') ? 'eye-slash' : 'eye'}`} /></button>
429
+            <button className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={status.get('hidden') ? 'false' : 'true'}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button>
429 430
           )}
430 431
         />
431 432
 

+ 2
- 1
app/javascript/mastodon/features/ui/components/boost_modal.js View File

@@ -8,6 +8,7 @@ import Avatar from '../../../components/avatar';
8 8
 import RelativeTimestamp from '../../../components/relative_timestamp';
9 9
 import DisplayName from '../../../components/display_name';
10 10
 import ImmutablePureComponent from 'react-immutable-pure-component';
11
+import Icon from 'mastodon/components/icon';
11 12
 
12 13
 const messages = defineMessages({
13 14
   reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
@@ -74,7 +75,7 @@ class BoostModal extends ImmutablePureComponent {
74 75
         </div>
75 76
 
76 77
         <div className='boost-modal__action-bar'>
77
-          <div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <i className='fa fa-retweet' /></span> }} /></div>
78
+          <div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} /></div>
78 79
           <Button text={intl.formatMessage(messages.reblog)} onClick={this.handleReblog} ref={this.setRef} />
79 80
         </div>
80 81
       </div>

+ 2
- 1
app/javascript/mastodon/features/ui/components/column_header.js View File

@@ -1,6 +1,7 @@
1 1
 import React from 'react';
2 2
 import PropTypes from 'prop-types';
3 3
 import classNames from 'classnames';
4
+import Icon from 'mastodon/components/icon';
4 5
 
5 6
 export default class ColumnHeader extends React.PureComponent {
6 7
 
@@ -21,7 +22,7 @@ export default class ColumnHeader extends React.PureComponent {
21 22
     let iconElement = '';
22 23
 
23 24
     if (icon) {
24
-      iconElement = <i className={`fa fa-fw fa-${icon} column-header__icon`} />;
25
+      iconElement = <Icon id={icon} fixedWidth className='column-header__icon' />;
25 26
     }
26 27
 
27 28
     return (

+ 3
- 2
app/javascript/mastodon/features/ui/components/column_link.js View File

@@ -1,6 +1,7 @@
1 1
 import React from 'react';
2 2
 import PropTypes from 'prop-types';
3 3
 import { Link } from 'react-router-dom';
4
+import Icon from 'mastodon/components/icon';
4 5
 
5 6
 const ColumnLink = ({ icon, text, to, href, method, badge }) => {
6 7
   const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null;
@@ -8,7 +9,7 @@ const ColumnLink = ({ icon, text, to, href, method, badge }) => {
8 9
   if (href) {
9 10
     return (
10 11
       <a href={href} className='column-link' data-method={method}>
11
-        <i className={`fa fa-fw fa-${icon} column-link__icon`} />
12
+        <Icon id={icon} fixedWidth className='column-link__icon' />
12 13
         {text}
13 14
         {badgeElement}
14 15
       </a>
@@ -16,7 +17,7 @@ const ColumnLink = ({ icon, text, to, href, method, badge }) => {
16 17
   } else {
17 18
     return (
18 19
       <Link to={to} className='column-link'>
19
-        <i className={`fa fa-fw fa-${icon} column-link__icon`} />
20
+        <Icon id={icon} fixedWidth className='column-link__icon' />
20 21
         {text}
21 22
         {badgeElement}
22 23
       </Link>

+ 2
- 1
app/javascript/mastodon/features/ui/components/columns_area.js View File

@@ -13,6 +13,7 @@ import ColumnLoading from './column_loading';
13 13
 import DrawerLoading from './drawer_loading';
14 14
 import BundleColumnError from './bundle_column_error';
15 15
 import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components';
16
+import Icon from 'mastodon/components/icon';
16 17
 
17 18
 import detectPassiveEvents from 'detect-passive-events';
18 19
 import { scrollRight } from '../../../scroll';
@@ -160,7 +161,7 @@ class ColumnsArea extends ImmutablePureComponent {
160 161
     this.pendingIndex = null;
161 162
 
162 163
     if (singleColumn) {
163
-      const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><i className='fa fa-pencil' /></Link>;
164
+      const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
164 165
 
165 166
       return columnIndex !== -1 ? [
166 167
         <ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>

+ 3
- 2
app/javascript/mastodon/features/ui/components/media_modal.js View File

@@ -9,6 +9,7 @@ import { defineMessages, injectIntl } from 'react-intl';
9 9
 import IconButton from '../../../components/icon_button';
10 10
 import ImmutablePureComponent from 'react-immutable-pure-component';
11 11
 import ImageLoader from './image_loader';
12
+import Icon from 'mastodon/components/icon';
12 13
 
13 14
 const messages = defineMessages({
14 15
   close: { id: 'lightbox.close', defaultMessage: 'Close' },
@@ -108,8 +109,8 @@ class MediaModal extends ImmutablePureComponent {
108 109
     const index = this.getIndex();
109 110
     let pagination = [];
110 111
 
111
-    const leftNav  = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><i className='fa fa-fw fa-chevron-left' /></button>;
112
-    const rightNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav  media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><i className='fa fa-fw fa-chevron-right' /></button>;
112
+    const leftNav  = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><Icon id='chevron-left' fixedWidth /></button>;
113
+    const rightNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav  media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><Icon id='chevron-right' fixedWidth /></button>;
113 114
 
114 115
     if (media.size > 1) {
115 116
       pagination = media.map((item, i) => {

+ 7
- 6
app/javascript/mastodon/features/ui/components/tabs_bar.js View File

@@ -4,16 +4,17 @@ import { NavLink, withRouter } from 'react-router-dom';
4 4
 import { FormattedMessage, injectIntl } from 'react-intl';
5 5
 import { debounce } from 'lodash';
6 6
 import { isUserTouching } from '../../../is_mobile';
7
+import Icon from 'mastodon/components/icon';
7 8
 
8 9
 export const links = [
9
-  <NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
10
-  <NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
10
+  <NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
11
+  <NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><Icon id='bell' fixedWidth /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
11 12
 
12
-  <NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
13
-  <NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
14
-  <NavLink className='tabs-bar__link primary' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><i className='fa fa-fw fa-search' /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
13
+  <NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
14
+  <NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
15
+  <NavLink className='tabs-bar__link primary' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
15 16
 
16
-  <NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><i className='fa fa-fw fa-bars' /></NavLink>,
17
+  <NavLink className='tabs-bar__link primary' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
17 18
 ];
18 19
 
19 20
 export function getIndex (path) {

+ 7
- 6
app/javascript/mastodon/features/video/index.js View File

@@ -6,6 +6,7 @@ import { throttle } from 'lodash';
6 6
 import classNames from 'classnames';
7 7
 import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
8 8
 import { displayMedia } from '../../initial_state';
9
+import Icon from 'mastodon/components/icon';
9 10
 
10 11
 const messages = defineMessages({
11 12
   play: { id: 'video.play', defaultMessage: 'Play' },
@@ -416,8 +417,8 @@ class Video extends React.PureComponent {
416 417
 
417 418
           <div className='video-player__buttons-bar'>
418 419
             <div className='video-player__buttons left'>
419
-              <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button>
420
-              <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button>
420
+              <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
421
+              <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
421 422
               <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
422 423
                 <div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
423 424
                 <span
@@ -437,10 +438,10 @@ class Video extends React.PureComponent {
437 438
             </div>
438 439
 
439 440
             <div className='video-player__buttons right'>
440
-              {!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>}
441
-              {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>}
442
-              {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-compress' /></button>}
443
-              <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button>
441
+              {!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye' fixedWidth /></button>}
442
+              {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
443
+              {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
444
+              <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
444 445
             </div>
445 446
           </div>
446 447
         </div>

+ 4
- 0
jest.config.js View File

@@ -22,4 +22,8 @@ module.exports = {
22 22
     '!app/javascript/mastodon/test_setup.js',
23 23
   ],
24 24
   coverageDirectory: '<rootDir>/coverage',
25
+  moduleDirectories: [
26
+    '<rootDir>/node_modules',
27
+    '<rootDir>/app/javascript',
28
+  ],
25 29
 };

Loading…
Cancel
Save