about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
authorStarfall <us@starfall.systems>2020-09-02 01:15:02 -0500
committerStarfall <us@starfall.systems>2020-09-02 01:15:02 -0500
commitfd0b806603dbd632d259d06af789c7195c7264dc (patch)
treea9ba1b09763ca15a96f927e361bb878b9833bc7d /app/javascript
parenta43c1d3f56c128c992f34b8e2b968de14e02ac48 (diff)
parentd967251fdc3826ad27d30e55258cfa4cdfd7c871 (diff)
Merge branch 'glitch' into main
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/flavours/glitch/features/list_timeline/index.js28
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js49
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js2
-rw-r--r--app/javascript/flavours/glitch/styles/components/index.scss20
-rw-r--r--app/javascript/flavours/glitch/util/emoji/index.js2
-rw-r--r--app/javascript/mastodon/actions/lists.js4
-rw-r--r--app/javascript/mastodon/features/emoji/emoji.js2
-rw-r--r--app/javascript/mastodon/features/getting_started/index.js37
-rw-r--r--app/javascript/mastodon/features/list_timeline/index.js30
-rw-r--r--app/javascript/mastodon/features/ui/components/focal_point_modal.js49
-rw-r--r--app/javascript/styles/mastodon/components.scss27
11 files changed, 143 insertions, 107 deletions
diff --git a/app/javascript/flavours/glitch/features/list_timeline/index.js b/app/javascript/flavours/glitch/features/list_timeline/index.js
index 908a65597..70e530bae 100644
--- a/app/javascript/flavours/glitch/features/list_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/list_timeline/index.js
@@ -14,13 +14,14 @@ import { openModal } from 'flavours/glitch/actions/modal';
 import MissingIndicator from 'flavours/glitch/components/missing_indicator';
 import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
 import Icon from 'flavours/glitch/components/icon';
+import RadioButton from 'flavours/glitch/components/radio_button';
 
 const messages = defineMessages({
   deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
   deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
-  all_replies:   { id: 'lists.replies_policy.all_replies', defaultMessage: 'any followed user' },
-  no_replies:    { id: 'lists.replies_policy.no_replies', defaultMessage: 'no one' },
-  list_replies:  { id: 'lists.replies_policy.list_replies', defaultMessage: 'members of the list' },
+  all_replies:   { id: 'lists.replies_policy.all_replies', defaultMessage: 'Any followed user' },
+  no_replies:    { id: 'lists.replies_policy.no_replies', defaultMessage: 'No one' },
+  list_replies:  { id: 'lists.replies_policy.list_replies', defaultMessage: 'Members of the list' },
 });
 
 const mapStateToProps = (state, props) => ({
@@ -176,7 +177,7 @@ class ListTimeline extends React.PureComponent {
           multiColumn={multiColumn}
           bindToDocument={!multiColumn}
         >
-          <div className='column-header__links'>
+          <div className='column-settings__row column-header__links'>
             <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}>
               <Icon id='pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
             </button>
@@ -187,19 +188,14 @@ class ListTimeline extends React.PureComponent {
           </div>
 
           { replies_policy !== undefined && (
-            <div>
+            <div role='group' aria-labelledby={`list-${id}-replies-policy`}>
+              <span id={`list-${id}-replies-policy`} className='column-settings__section'>
+                <FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' />
+              </span>
               <div className='column-settings__row'>
-                <fieldset>
-                  <legend><FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' /></legend>
-                  { ['no_replies', 'list_replies', 'all_replies'].map(policy => (
-                    <div className='setting-radio'>
-                      <input className='setting-radio__input' id={['setting', 'radio', id, policy].join('-')} type='radio' value={policy} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
-                      <label className='setting-radio__label' htmlFor={['setting', 'radio', id, policy].join('-')}>
-                        <FormattedMessage {...messages[policy]} />
-                      </label>
-                    </div>
-                  ))}
-                </fieldset>
+                { ['no_replies', 'list_replies', 'all_replies'].map(policy => (
+                  <RadioButton name='order' value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
+                ))}
               </div>
             </div>
           )}
diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js
index c8b0e4bd7..b790b29a0 100644
--- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js
@@ -18,6 +18,8 @@ import { length } from 'stringz';
 import { Tesseract as fetchTesseract } from 'flavours/glitch/util/async-components';
 import GIFV from 'flavours/glitch/components/gifv';
 import { me } from 'flavours/glitch/util/initial_state';
+import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js';
+import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js';
 
 const messages = defineMessages({
   close: { id: 'lightbox.close', defaultMessage: 'Close' },
@@ -104,6 +106,7 @@ class FocalPointModal extends ImmutablePureComponent {
     dirty: false,
     progress: 0,
     loading: true,
+    ocrStatus: '',
   };
 
   componentWillMount () {
@@ -219,11 +222,18 @@ class FocalPointModal extends ImmutablePureComponent {
 
     this.setState({ detecting: true });
 
-    fetchTesseract().then(({ TesseractWorker }) => {
-      const worker = new TesseractWorker({
-        workerPath: `${assetHost}/packs/ocr/worker.min.js`,
-        corePath: `${assetHost}/packs/ocr/tesseract-core.wasm.js`,
-        langPath: `${assetHost}/ocr/lang-data`,
+    fetchTesseract().then(({ createWorker }) => {
+      const worker = createWorker({
+        workerPath: tesseractWorkerPath,
+        corePath: tesseractCorePath,
+        langPath: assetHost,
+        logger: ({ status, progress }) => {
+          if (status === 'recognizing text') {
+            this.setState({ ocrStatus: 'detecting', progress });
+          } else {
+            this.setState({ ocrStatus: 'preparing', progress });
+          }
+        },
       });
 
       let media_url = media.get('url');
@@ -236,12 +246,18 @@ class FocalPointModal extends ImmutablePureComponent {
         }
       }
 
-      worker.recognize(media_url)
-        .progress(({ progress }) => this.setState({ progress }))
-        .finally(() => worker.terminate())
-        .then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false }))
-        .catch(() => this.setState({ detecting: false }));
-    }).catch(() => this.setState({ detecting: false }));
+      (async () => {
+        await worker.load();
+        await worker.loadLanguage('eng');
+        await worker.initialize('eng');
+        const { data: { text } } = await worker.recognize(media_url);
+        this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false });
+        await worker.terminate();
+      })();
+    }).catch((e) => {
+      console.error(e);
+      this.setState({ detecting: false });
+    });
   }
 
   handleThumbnailChange = e => {
@@ -261,7 +277,7 @@ class FocalPointModal extends ImmutablePureComponent {
 
   render () {
     const { media, intl, account, onClose, isUploadingThumbnail } = this.props;
-    const { x, y, dragging, description, dirty, detecting, progress } = this.state;
+    const { x, y, dragging, description, dirty, detecting, progress, ocrStatus } = this.state;
 
     const width  = media.getIn(['meta', 'original', 'width']) || null;
     const height = media.getIn(['meta', 'original', 'height']) || null;
@@ -282,6 +298,13 @@ class FocalPointModal extends ImmutablePureComponent {
       descriptionLabel = <FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' />;
     }
 
+    let ocrMessage = '';
+    if (ocrStatus === 'detecting') {
+      ocrMessage = <FormattedMessage id='upload_modal.analyzing_picture' defaultMessage='Analyzing picture…' />;
+    } else {
+      ocrMessage = <FormattedMessage id='upload_modal.preparing_ocr' defaultMessage='Preparing OCR…' />;
+    }
+
     return (
       <div className='modal-root__modal report-modal' style={{ maxWidth: 960 }}>
         <div className='report-modal__target'>
@@ -333,7 +356,7 @@ class FocalPointModal extends ImmutablePureComponent {
               />
 
               <div className='setting-text__modifiers'>
-                <UploadProgress progress={progress * 100} active={detecting} icon='file-text-o' message={<FormattedMessage id='upload_modal.analyzing_picture' defaultMessage='Analyzing picture…' />} />
+                <UploadProgress progress={progress * 100} active={detecting} icon='file-text-o' message={ocrMessage} />
               </div>
             </div>
 
diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index 478883f91..e081c31ad 100644
--- a/app/javascript/flavours/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -184,7 +184,7 @@ function continueThread (state, status) {
     map.set('in_reply_to', status.id);
     map.update(
       'advanced_options',
-      map => map.merge(new ImmutableMap({ do_not_federate: status.get('local_only') }))
+      map => map.merge(new ImmutableMap({ do_not_federate: status.local_only }))
     );
     map.set('privacy', status.visibility);
     map.set('sensitive', false);
diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss
index 2231874bc..306e62342 100644
--- a/app/javascript/flavours/glitch/styles/components/index.scss
+++ b/app/javascript/flavours/glitch/styles/components/index.scss
@@ -1356,7 +1356,6 @@ button.icon-button.active i.fa-retweet {
 }
 
 .setting-toggle__label,
-.setting-radio__label,
 .setting-meta__label {
   color: $darker-text-color;
   display: inline-block;
@@ -1365,25 +1364,8 @@ button.icon-button.active i.fa-retweet {
   vertical-align: middle;
 }
 
-.setting-radio {
+.column-settings__row .radio-button {
   display: block;
-  line-height: 18px;
-}
-
-.setting-radio__label {
-  margin-bottom: 0;
-}
-
-.column-settings__row legend {
-  color: $darker-text-color;
-  cursor: default;
-  display: block;
-  font-weight: 500;
-  margin-top: 10px;
-}
-
-.setting-radio__input {
-  vertical-align: middle;
 }
 
 .setting-meta__label {
diff --git a/app/javascript/flavours/glitch/util/emoji/index.js b/app/javascript/flavours/glitch/util/emoji/index.js
index 61f211c92..a59e17ddb 100644
--- a/app/javascript/flavours/glitch/util/emoji/index.js
+++ b/app/javascript/flavours/glitch/util/emoji/index.js
@@ -12,7 +12,7 @@ const emojiFilenames = (emojis) => {
 };
 
 // Emoji requiring extra borders depending on theme
-const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂‍♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂‍♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴']);
+const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂‍♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂‍♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞']);
 const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️']);
 
 const emojiFilename = (filename) => {
diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js
index d736bacef..5ab922436 100644
--- a/app/javascript/mastodon/actions/lists.js
+++ b/app/javascript/mastodon/actions/lists.js
@@ -150,10 +150,10 @@ export const createListFail = error => ({
   error,
 });
 
-export const updateList = (id, title, shouldReset) => (dispatch, getState) => {
+export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => {
   dispatch(updateListRequest(id));
 
-  api(getState).put(`/api/v1/lists/${id}`, { title }).then(({ data }) => {
+  api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).then(({ data }) => {
     dispatch(updateListSuccess(data));
 
     if (shouldReset) {
diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js
index f7d3cfd08..5237b25f0 100644
--- a/app/javascript/mastodon/features/emoji/emoji.js
+++ b/app/javascript/mastodon/features/emoji/emoji.js
@@ -12,7 +12,7 @@ const emojiFilenames = (emojis) => {
 };
 
 // Emoji requiring extra borders depending on theme
-const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂‍♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂‍♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴']);
+const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂‍♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂‍♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞']);
 const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️']);
 
 const emojiFilename = (filename) => {
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index d9838e1c7..02e1bbba5 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -40,6 +40,7 @@ const messages = defineMessages({
 
 const mapStateToProps = state => ({
   myAccount: state.getIn(['accounts', me]),
+  columns: state.getIn(['settings', 'columns']),
   unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
 });
 
@@ -89,60 +90,66 @@ class GettingStarted extends ImmutablePureComponent {
   }
 
   render () {
-    const { intl, myAccount, multiColumn, unreadFollowRequests } = this.props;
+    const { intl, myAccount, columns, multiColumn, unreadFollowRequests } = this.props;
 
     const navItems = [];
-    let i = 1;
     let height = (multiColumn) ? 0 : 60;
 
     if (multiColumn) {
       navItems.push(
-        <ColumnSubheading key={i++} text={intl.formatMessage(messages.discover)} />,
-        <ColumnLink key={i++} icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />,
-        <ColumnLink key={i++} icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />,
+        <ColumnSubheading key='header-discover' text={intl.formatMessage(messages.discover)} />,
+        <ColumnLink key='community_timeline' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />,
+        <ColumnLink key='public_timeline' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />,
       );
 
       height += 34 + 48*2;
 
       if (profile_directory) {
         navItems.push(
-          <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
+          <ColumnLink key='directory' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
         );
 
         height += 48;
       }
 
       navItems.push(
-        <ColumnSubheading key={i++} text={intl.formatMessage(messages.personal)} />,
+        <ColumnSubheading key='header-personal' text={intl.formatMessage(messages.personal)} />,
       );
 
       height += 34;
     } else if (profile_directory) {
       navItems.push(
-        <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
+        <ColumnLink key='directory' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
       );
 
       height += 48;
     }
 
+    if (multiColumn && !columns.find(item => item.get('id') === 'HOME')) {
+      navItems.push(
+        <ColumnLink key='home' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/timelines/home' />,
+      );
+      height += 48;
+    }
+
     navItems.push(
-      <ColumnLink key={i++} icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />,
-      <ColumnLink key={i++} icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />,
-      <ColumnLink key={i++} icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
-      <ColumnLink key={i++} icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />,
+      <ColumnLink key='direct' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />,
+      <ColumnLink key='bookmark' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />,
+      <ColumnLink key='favourites' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
+      <ColumnLink key='lists' icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />,
     );
 
     height += 48*4;
 
     if (myAccount.get('locked') || unreadFollowRequests > 0) {
-      navItems.push(<ColumnLink key={i++} icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
+      navItems.push(<ColumnLink key='follow_requests' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
       height += 48;
     }
 
     if (!multiColumn) {
       navItems.push(
-        <ColumnSubheading key={i++} text={intl.formatMessage(messages.settings_subheading)} />,
-        <ColumnLink key={i++} icon='gears' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />,
+        <ColumnSubheading key='header-settings' text={intl.formatMessage(messages.settings_subheading)} />,
+        <ColumnLink key='preferences' icon='gears' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />,
       );
 
       height += 34 + 48;
diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js
index f3205b2bf..a3be8fbea 100644
--- a/app/javascript/mastodon/features/list_timeline/index.js
+++ b/app/javascript/mastodon/features/list_timeline/index.js
@@ -10,15 +10,19 @@ import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
 import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
 import { connectListStream } from '../../actions/streaming';
 import { expandListTimeline } from '../../actions/timelines';
-import { fetchList, deleteList } from '../../actions/lists';
+import { fetchList, deleteList, updateList } from '../../actions/lists';
 import { openModal } from '../../actions/modal';
 import MissingIndicator from '../../components/missing_indicator';
 import LoadingIndicator from '../../components/loading_indicator';
 import Icon from 'mastodon/components/icon';
+import RadioButton from 'mastodon/components/radio_button';
 
 const messages = defineMessages({
   deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
   deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
+  all_replies:   { id: 'lists.replies_policy.all_replies', defaultMessage: 'Any followed user' },
+  no_replies:    { id: 'lists.replies_policy.no_replies', defaultMessage: 'No one' },
+  list_replies:  { id: 'lists.replies_policy.list_replies', defaultMessage: 'Members of the list' },
 });
 
 const mapStateToProps = (state, props) => ({
@@ -131,11 +135,18 @@ class ListTimeline extends React.PureComponent {
     }));
   }
 
+  handleRepliesPolicyChange = ({ target }) => {
+    const { dispatch } = this.props;
+    const { id } = this.props.params;
+    dispatch(updateList(id, undefined, false, target.value));
+  }
+
   render () {
-    const { shouldUpdateScroll, hasUnread, columnId, multiColumn, list } = this.props;
+    const { shouldUpdateScroll, hasUnread, columnId, multiColumn, list, intl } = this.props;
     const { id } = this.props.params;
     const pinned = !!columnId;
     const title  = list ? list.get('title') : id;
+    const replies_policy = list ? list.get('replies_policy') : undefined;
 
     if (typeof list === 'undefined') {
       return (
@@ -166,7 +177,7 @@ class ListTimeline extends React.PureComponent {
           pinned={pinned}
           multiColumn={multiColumn}
         >
-          <div className='column-header__links'>
+          <div className='column-settings__row column-header__links'>
             <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}>
               <Icon id='pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
             </button>
@@ -175,6 +186,19 @@ class ListTimeline extends React.PureComponent {
               <Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
             </button>
           </div>
+
+          { replies_policy !== undefined && (
+            <div role='group' aria-labelledby={`list-${id}-replies-policy`}>
+              <span id={`list-${id}-replies-policy`} className='column-settings__section'>
+                <FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' />
+              </span>
+              <div className='column-settings__row'>
+                { ['no_replies', 'list_replies', 'all_replies'].map(policy => (
+                  <RadioButton name='order' value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
+                ))}
+              </div>
+            </div>
+          )}
         </ColumnHeader>
 
         <StatusListContainer
diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
index 7348d9599..926a495d1 100644
--- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js
+++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
@@ -18,6 +18,8 @@ import { length } from 'stringz';
 import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
 import GIFV from 'mastodon/components/gifv';
 import { me } from 'mastodon/initial_state';
+import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js';
+import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js';
 
 const messages = defineMessages({
   close: { id: 'lightbox.close', defaultMessage: 'Close' },
@@ -104,6 +106,7 @@ class FocalPointModal extends ImmutablePureComponent {
     dirty: false,
     progress: 0,
     loading: true,
+    ocrStatus: '',
   };
 
   componentWillMount () {
@@ -219,11 +222,18 @@ class FocalPointModal extends ImmutablePureComponent {
 
     this.setState({ detecting: true });
 
-    fetchTesseract().then(({ TesseractWorker }) => {
-      const worker = new TesseractWorker({
-        workerPath: `${assetHost}/packs/ocr/worker.min.js`,
-        corePath: `${assetHost}/packs/ocr/tesseract-core.wasm.js`,
-        langPath: `${assetHost}/ocr/lang-data`,
+    fetchTesseract().then(({ createWorker }) => {
+      const worker = createWorker({
+        workerPath: tesseractWorkerPath,
+        corePath: tesseractCorePath,
+        langPath: assetHost,
+        logger: ({ status, progress }) => {
+          if (status === 'recognizing text') {
+            this.setState({ ocrStatus: 'detecting', progress });
+          } else {
+            this.setState({ ocrStatus: 'preparing', progress });
+          }
+        },
       });
 
       let media_url = media.get('url');
@@ -236,12 +246,18 @@ class FocalPointModal extends ImmutablePureComponent {
         }
       }
 
-      worker.recognize(media_url)
-        .progress(({ progress }) => this.setState({ progress }))
-        .finally(() => worker.terminate())
-        .then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false }))
-        .catch(() => this.setState({ detecting: false }));
-    }).catch(() => this.setState({ detecting: false }));
+      (async () => {
+        await worker.load();
+        await worker.loadLanguage('eng');
+        await worker.initialize('eng');
+        const { data: { text } } = await worker.recognize(media_url);
+        this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false });
+        await worker.terminate();
+      })();
+    }).catch((e) => {
+      console.error(e);
+      this.setState({ detecting: false });
+    });
   }
 
   handleThumbnailChange = e => {
@@ -261,7 +277,7 @@ class FocalPointModal extends ImmutablePureComponent {
 
   render () {
     const { media, intl, account, onClose, isUploadingThumbnail } = this.props;
-    const { x, y, dragging, description, dirty, detecting, progress } = this.state;
+    const { x, y, dragging, description, dirty, detecting, progress, ocrStatus } = this.state;
 
     const width  = media.getIn(['meta', 'original', 'width']) || null;
     const height = media.getIn(['meta', 'original', 'height']) || null;
@@ -282,6 +298,13 @@ class FocalPointModal extends ImmutablePureComponent {
       descriptionLabel = <FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' />;
     }
 
+    let ocrMessage = '';
+    if (ocrStatus === 'detecting') {
+      ocrMessage = <FormattedMessage id='upload_modal.analyzing_picture' defaultMessage='Analyzing picture…' />;
+    } else {
+      ocrMessage = <FormattedMessage id='upload_modal.preparing_ocr' defaultMessage='Preparing OCR…' />;
+    }
+
     return (
       <div className='modal-root__modal report-modal' style={{ maxWidth: 960 }}>
         <div className='report-modal__target'>
@@ -333,7 +356,7 @@ class FocalPointModal extends ImmutablePureComponent {
               />
 
               <div className='setting-text__modifiers'>
-                <UploadProgress progress={progress * 100} active={detecting} icon='file-text-o' message={<FormattedMessage id='upload_modal.analyzing_picture' defaultMessage='Analyzing picture…' />} />
+                <UploadProgress progress={progress * 100} active={detecting} icon='file-text-o' message={ocrMessage} />
               </div>
             </div>
 
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index f6deadffa..4f4507b37 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -980,14 +980,6 @@
     outline: 0;
     background: lighten($ui-base-color, 4%);
 
-    .status.status-direct {
-      background: lighten($ui-base-color, 12%);
-
-      &.muted {
-        background: transparent;
-      }
-    }
-
     .detailed-status,
     .detailed-status__action-bar {
       background: lighten($ui-base-color, 8%);
@@ -1022,11 +1014,6 @@
     margin-top: 8px;
   }
 
-  &.status-direct:not(.read) {
-    background: lighten($ui-base-color, 8%);
-    border-bottom-color: lighten($ui-base-color, 12%);
-  }
-
   &.light {
     .status__relative-time,
     .status__visibility-icon {
@@ -1064,16 +1051,6 @@
   }
 }
 
-.notification-favourite {
-  .status.status-direct {
-    background: transparent;
-
-    .icon-button.disabled {
-      color: lighten($action-button-color, 13%);
-    }
-  }
-}
-
 .status__relative-time,
 .status__visibility-icon,
 .notification__relative_time {
@@ -5957,6 +5934,10 @@ a.status-card.compact:hover {
   }
 }
 
+.column-settings__row .radio-button {
+  display: block;
+}
+
 .radio-button {
   font-size: 14px;
   position: relative;