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 5 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