about summary refs log tree commit diff
path: root/app/javascript/flavours
diff options
context:
space:
mode:
authorpluralcafe-docker <docker@plural.cafe>2018-08-30 05:23:58 +0000
committerpluralcafe-docker <docker@plural.cafe>2018-08-30 05:23:58 +0000
commitcc7437e25597e24b9a5f06f7991861506d9abe5c (patch)
treee627d32df29ef7ae30a67607caf3ecdc1ae333a9 /app/javascript/flavours
parent395164add468b1079669699dfe8eeaab73f69c15 (diff)
parent5ce67276691c37baad149f2f89f765543f70e6f9 (diff)
Merge branch 'glitch'
Diffstat (limited to 'app/javascript/flavours')
-rw-r--r--app/javascript/flavours/glitch/actions/compose.js75
-rw-r--r--app/javascript/flavours/glitch/actions/store.js13
-rw-r--r--app/javascript/flavours/glitch/components/extended_video_player.js1
-rw-r--r--app/javascript/flavours/glitch/components/media_gallery.js1
-rw-r--r--app/javascript/flavours/glitch/components/status.js2
-rw-r--r--app/javascript/flavours/glitch/features/account/components/header.js4
-rw-r--r--app/javascript/flavours/glitch/features/composer/index.js8
-rw-r--r--app/javascript/flavours/glitch/features/composer/options/index.js16
-rw-r--r--app/javascript/flavours/glitch/features/composer/textarea/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js68
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/page/index.js8
-rw-r--r--app/javascript/flavours/glitch/features/report/components/status_check_box.js1
-rw-r--r--app/javascript/flavours/glitch/features/status/components/detailed_status.js1
-rw-r--r--app/javascript/flavours/glitch/features/video/index.js1
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js25
-rw-r--r--app/javascript/flavours/glitch/reducers/local_settings.js1
-rw-r--r--app/javascript/flavours/glitch/styles/components/metadata.scss2
-rw-r--r--app/javascript/flavours/glitch/util/hashtag.js8
-rw-r--r--app/javascript/flavours/glitch/util/settings.js1
19 files changed, 183 insertions, 55 deletions
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index f6c8086fe..fb311fc0a 100644
--- a/app/javascript/flavours/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -3,6 +3,8 @@ import { CancelToken } from 'axios';
 import { throttle } from 'lodash';
 import { search as emojiSearch } from 'flavours/glitch/util/emoji/emoji_mart_search_light';
 import { useEmoji } from './emojis';
+import { tagHistory } from 'flavours/glitch/util/settings';
+import { recoverHashtags } from 'flavours/glitch/util/hashtag';
 import resizeImage from 'flavours/glitch/util/resize_image';
 
 import { updateTimeline } from './timelines';
@@ -28,6 +30,9 @@ export const COMPOSE_UPLOAD_UNDO     = 'COMPOSE_UPLOAD_UNDO';
 export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
 export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
 export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
+export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
+
+export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
 
 export const COMPOSE_MOUNT   = 'COMPOSE_MOUNT';
 export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
@@ -136,6 +141,7 @@ export function submitCompose() {
         'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
       },
     }).then(function (response) {
+      dispatch(insertIntoTagHistory(response.data.tags, status));
       dispatch(submitComposeSuccess({ ...response.data }));
 
       //  If the response has no data then we can't do anything else.
@@ -315,12 +321,22 @@ const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => {
   dispatch(readyComposeSuggestionsEmojis(token, results));
 };
 
+const fetchComposeSuggestionsTags = (dispatch, getState, token) => {
+  dispatch(updateSuggestionTags(token));
+};
+
 export function fetchComposeSuggestions(token) {
   return (dispatch, getState) => {
-    if (token[0] === ':') {
+    switch (token[0]) {
+    case ':':
       fetchComposeSuggestionsEmojis(dispatch, getState, token);
-    } else {
+      break;
+    case '#':
+      fetchComposeSuggestionsTags(dispatch, getState, token);
+      break;
+    default:
       fetchComposeSuggestionsAccounts(dispatch, getState, token);
+      break;
     }
   };
 };
@@ -343,10 +359,15 @@ export function readyComposeSuggestionsAccounts(token, accounts) {
 
 export function selectComposeSuggestion(position, token, suggestion) {
   return (dispatch, getState) => {
-    const completion = typeof suggestion === 'object' && suggestion.id ? (
-      dispatch(useEmoji(suggestion)),
-      suggestion.native || suggestion.colons
-    ) : '@' + getState().getIn(['accounts', suggestion, 'acct']);
+    let completion;
+    if (typeof suggestion === 'object' && suggestion.id) {
+      dispatch(useEmoji(suggestion));
+      completion = suggestion.native || suggestion.colons;
+    } else if (suggestion[0] === '#') {
+      completion = suggestion;
+    } else {
+      completion = '@' + getState().getIn(['accounts', suggestion, 'acct']);
+    }
 
     dispatch({
       type: COMPOSE_SUGGESTION_SELECT,
@@ -357,6 +378,48 @@ export function selectComposeSuggestion(position, token, suggestion) {
   };
 };
 
+export function updateSuggestionTags(token) {
+  return {
+    type: COMPOSE_SUGGESTION_TAGS_UPDATE,
+    token,
+  };
+}
+
+export function updateTagHistory(tags) {
+  return {
+    type: COMPOSE_TAG_HISTORY_UPDATE,
+    tags,
+  };
+}
+
+export function hydrateCompose() {
+  return (dispatch, getState) => {
+    const me = getState().getIn(['meta', 'me']);
+    const history = tagHistory.get(me);
+
+    if (history !== null) {
+      dispatch(updateTagHistory(history));
+    }
+  };
+}
+
+function insertIntoTagHistory(recognizedTags, text) {
+  return (dispatch, getState) => {
+    const state = getState();
+    const oldHistory = state.getIn(['compose', 'tagHistory']);
+    const me = state.getIn(['meta', 'me']);
+    const names = recoverHashtags(recognizedTags, text);
+    const intersectedOldHistory = oldHistory.filter(name => names.findIndex(newName => newName.toLowerCase() === name.toLowerCase()) === -1);
+
+    names.push(...intersectedOldHistory.toJS());
+
+    const newHistory = names.slice(0, 1000);
+
+    tagHistory.set(me, newHistory);
+    dispatch(updateTagHistory(newHistory));
+  };
+}
+
 export function mountCompose() {
   return {
     type: COMPOSE_MOUNT,
diff --git a/app/javascript/flavours/glitch/actions/store.js b/app/javascript/flavours/glitch/actions/store.js
index a1db0fdd5..2dd94a998 100644
--- a/app/javascript/flavours/glitch/actions/store.js
+++ b/app/javascript/flavours/glitch/actions/store.js
@@ -1,4 +1,5 @@
 import { Iterable, fromJS } from 'immutable';
+import { hydrateCompose } from './compose';
 
 export const STORE_HYDRATE = 'STORE_HYDRATE';
 export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
@@ -8,10 +9,14 @@ const convertState = rawState =>
     Iterable.isIndexed(v) ? v.toList() : v.toMap());
 
 export function hydrateStore(rawState) {
-  const state = convertState(rawState);
+  return dispatch => {
+    const state = convertState(rawState);
 
-  return {
-    type: STORE_HYDRATE,
-    state,
+    dispatch({
+      type: STORE_HYDRATE,
+      state,
+    });
+
+    dispatch(hydrateCompose());
   };
 };
diff --git a/app/javascript/flavours/glitch/components/extended_video_player.js b/app/javascript/flavours/glitch/components/extended_video_player.js
index 9e2f6835a..009c0d559 100644
--- a/app/javascript/flavours/glitch/components/extended_video_player.js
+++ b/app/javascript/flavours/glitch/components/extended_video_player.js
@@ -50,6 +50,7 @@ export default class ExtendedVideoPlayer extends React.PureComponent {
           role='button'
           tabIndex='0'
           aria-label={alt}
+          title={alt}
           muted={muted}
           controls={controls}
           loop={!controls}
diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js
index d87be14cc..3faf0b453 100644
--- a/app/javascript/flavours/glitch/components/media_gallery.js
+++ b/app/javascript/flavours/glitch/components/media_gallery.js
@@ -174,6 +174,7 @@ class Item extends React.PureComponent {
           <video
             className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`}
             aria-label={attachment.get('description')}
+            title={attachment.get('description')}
             role='application'
             src={attachment.get('url')}
             onClick={this.handleClick}
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index a87721ef8..1ac5a4b3e 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -412,6 +412,7 @@ export default class Status extends ImmutablePureComponent {
             {Component => (<Component
               preview={video.get('preview_url')}
               src={video.get('url')}
+              alt={video.get('description')}
               inline
               sensitive={status.get('sensitive')}
               letterbox={settings.getIn(['media', 'letterbox'])}
@@ -474,6 +475,7 @@ export default class Status extends ImmutablePureComponent {
     const computedClass = classNames('status', `status-${status.get('visibility')}`, {
       collapsed: isCollapsed,
       'has-background': isCollapsed && background,
+      'status__wrapper-reply': !!status.get('in_reply_to_id'),
       muted,
     }, 'focusable');
 
diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js
index 174df0cc9..eda0d637e 100644
--- a/app/javascript/flavours/glitch/features/account/components/header.js
+++ b/app/javascript/flavours/glitch/features/account/components/header.js
@@ -56,7 +56,9 @@ export default class Header extends ImmutablePureComponent {
     }
 
     if (me !== account.get('id')) {
-      if (account.getIn(['relationship', 'requested'])) {
+      if (!account.get('relationship')) { // Wait until the relationship is loaded
+        actionBtn = '';
+      } else if (account.getIn(['relationship', 'requested'])) {
         actionBtn = (
           <div className='account--action-button'>
             <IconButton size={26} active icon='hourglass' title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />
diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js
index f312e9d59..cf6f45b34 100644
--- a/app/javascript/flavours/glitch/features/composer/index.js
+++ b/app/javascript/flavours/glitch/features/composer/index.js
@@ -51,6 +51,7 @@ import { privacyPreference } from 'flavours/glitch/util/privacy_preference';
 
 //  State mapping.
 function mapStateToProps (state) {
+  const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']);
   const inReplyTo = state.getIn(['compose', 'in_reply_to']);
   const replyPrivacy = inReplyTo ? state.getIn(['statuses', inReplyTo, 'visibility']) : null;
   const sideArmBasePrivacy = state.getIn(['local_settings', 'side_arm']);
@@ -85,12 +86,13 @@ function mapStateToProps (state) {
     sideArm: sideArmPrivacy,
     sensitive: state.getIn(['compose', 'sensitive']),
     showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
-    spoiler: state.getIn(['compose', 'spoiler']),
+    spoiler: spoilersAlwaysOn || state.getIn(['compose', 'spoiler']),
     spoilerText: state.getIn(['compose', 'spoiler_text']),
     suggestionToken: state.getIn(['compose', 'suggestion_token']),
     suggestions: state.getIn(['compose', 'suggestions']),
     text: state.getIn(['compose', 'text']),
     anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
+    spoilersAlwaysOn: spoilersAlwaysOn,
   };
 };
 
@@ -376,6 +378,7 @@ class Composer extends React.Component {
       spoilerText,
       suggestions,
       text,
+      spoilersAlwaysOn,
     } = this.props;
 
     let disabledButton = isSubmitting || isUploading || (!!text.length && !text.trim().length && !anyMedia);
@@ -443,7 +446,7 @@ class Composer extends React.Component {
           onDoodleOpen={onOpenDoodleModal}
           onModalClose={onCloseModal}
           onModalOpen={onOpenActionsModal}
-          onToggleSpoiler={onChangeSpoilerness}
+          onToggleSpoiler={spoilersAlwaysOn ? null : onChangeSpoilerness}
           onUpload={onUpload}
           privacy={privacy}
           resetFileKey={resetFileKey}
@@ -515,6 +518,7 @@ Composer.propTypes = {
   onUnmount: PropTypes.func,
   onUpload: PropTypes.func,
   anyMedia: PropTypes.bool,
+  spoilersAlwaysOn: PropTypes.bool,
 };
 
 //  Connecting and export.
diff --git a/app/javascript/flavours/glitch/features/composer/options/index.js b/app/javascript/flavours/glitch/features/composer/options/index.js
index c129622bc..05cbe24c9 100644
--- a/app/javascript/flavours/glitch/features/composer/options/index.js
+++ b/app/javascript/flavours/glitch/features/composer/options/index.js
@@ -285,13 +285,15 @@ export default class ComposerOptions extends React.PureComponent {
           title={intl.formatMessage(messages.change_privacy)}
           value={privacy}
         />
-        <TextIconButton
-          active={spoiler}
-          ariaControls='glitch.composer.spoiler.input'
-          label='CW'
-          onClick={onToggleSpoiler}
-          title={intl.formatMessage(messages.spoiler)}
-        />
+        {onToggleSpoiler && (
+          <TextIconButton
+            active={spoiler}
+            ariaControls='glitch.composer.spoiler.input'
+            label='CW'
+            onClick={onToggleSpoiler}
+            title={intl.formatMessage(messages.spoiler)}
+          />
+        )}
         <Dropdown
           active={advancedOptions && advancedOptions.some(value => !!value)}
           disabled={disabled}
diff --git a/app/javascript/flavours/glitch/features/composer/textarea/index.js b/app/javascript/flavours/glitch/features/composer/textarea/index.js
index 51d44a83b..50e46fc78 100644
--- a/app/javascript/flavours/glitch/features/composer/textarea/index.js
+++ b/app/javascript/flavours/glitch/features/composer/textarea/index.js
@@ -58,7 +58,7 @@ const handlers = {
     const right = value.slice(selectionStart).search(/[\s\u200B]/);
     const token = function () {
       switch (true) {
-      case left < 0 || !/[@:]/.test(value[left]):
+      case left < 0 || !/[@:#]/.test(value[left]):
         return null;
       case right < 0:
         return value.slice(left);
diff --git a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js
index f55640bcf..331692398 100644
--- a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js
+++ b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js
@@ -57,6 +57,42 @@ export default class ComposerTextareaSuggestionsItem extends React.Component {
     } = this.props;
     const computedClass = classNames('composer--textarea--suggestions--item', { selected });
 
+    //  If the suggestion is an object, then we render an emoji.
+    //  Otherwise, we render a hashtag if it starts with #, or an account.
+    let inner;
+    if (typeof suggestion === 'object') {
+      let url;
+      if (suggestion.custom) {
+        url = suggestion.imageUrl;
+      } else {
+        const mapping = unicodeMapping[suggestion.native] || unicodeMapping[suggestion.native.replace(/\uFE0F$/, '')];
+        if (mapping) {
+          url = `${assetHost}/emoji/${mapping.filename}.svg`;
+        }
+      }
+      if (url) {
+        inner = (
+          <div className='emoji'>
+            <img
+              alt={suggestion.native || suggestion.colons}
+              className='emojione'
+              src={url}
+            />
+            {suggestion.colons}
+          </div>
+        );
+      }
+    } else if (suggestion[0] === '#') {
+      inner = suggestion;
+    } else {
+      inner = (
+        <AccountContainer
+          id={suggestion}
+          small
+        />
+      );
+    }
+
     //  The result.
     return (
       <div
@@ -66,37 +102,7 @@ export default class ComposerTextareaSuggestionsItem extends React.Component {
         role='button'
         tabIndex='0'
       >
-        { //  If the suggestion is an object, then we render an emoji.
-          //  Otherwise, we render an account.
-          typeof suggestion === 'object' ? function () {
-            const url = function () {
-              if (suggestion.custom) {
-                return suggestion.imageUrl;
-              } else {
-                const mapping = unicodeMapping[suggestion.native] || unicodeMapping[suggestion.native.replace(/\uFE0F$/, '')];
-                if (!mapping) {
-                  return null;
-                }
-                return `${assetHost}/emoji/${mapping.filename}.svg`;
-              }
-            }();
-            return url ? (
-              <div className='emoji'>
-                <img
-                  alt={suggestion.native || suggestion.colons}
-                  className='emojione'
-                  src={url}
-                />
-                {suggestion.colons}
-              </div>
-            ) : null;
-          }() : (
-            <AccountContainer
-              id={suggestion}
-              small
-            />
-          )
-        }
+        { inner }
       </div>
     );
   }
diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js
index f88e23c47..ad5c11979 100644
--- a/app/javascript/flavours/glitch/features/local_settings/page/index.js
+++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js
@@ -77,6 +77,14 @@ export default class LocalSettingsPage extends React.PureComponent {
           <h2><FormattedMessage id='settings.compose_box_opts' defaultMessage='Compose box options' /></h2>
           <LocalSettingsPageItem
             settings={settings}
+            item={['always_show_spoilers_field']}
+            id='mastodon-settings--always_show_spoilers_field'
+            onChange={onChange}
+          >
+            <FormattedMessage id='settings.always_show_spoilers_field' defaultMessage='Always enable the Content Warning field' />
+          </LocalSettingsPageItem>
+          <LocalSettingsPageItem
+            settings={settings}
             item={['side_arm']}
             id='mastodon-settings--side_arm'
             options={[
diff --git a/app/javascript/flavours/glitch/features/report/components/status_check_box.js b/app/javascript/flavours/glitch/features/report/components/status_check_box.js
index a685132b0..d674eecf9 100644
--- a/app/javascript/flavours/glitch/features/report/components/status_check_box.js
+++ b/app/javascript/flavours/glitch/features/report/components/status_check_box.js
@@ -36,6 +36,7 @@ export default class StatusCheckBox extends React.PureComponent {
               <Component
                 preview={video.get('preview_url')}
                 src={video.get('url')}
+                alt={video.get('description')}
                 width={239}
                 height={110}
                 inline
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
index 5cfc9dfae..ee9eb02c6 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -60,6 +60,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
           <Video
             preview={video.get('preview_url')}
             src={video.get('url')}
+            alt={video.get('description')}
             inline
             sensitive={status.get('sensitive')}
             letterbox={settings.getIn(['media', 'letterbox'])}
diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js
index a578df026..7e284a0bc 100644
--- a/app/javascript/flavours/glitch/features/video/index.js
+++ b/app/javascript/flavours/glitch/features/video/index.js
@@ -323,6 +323,7 @@ export default class Video extends React.PureComponent {
           role='button'
           tabIndex='0'
           aria-label={alt}
+          title={alt}
           width={width}
           height={height}
           onClick={this.togglePlay}
diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index 8b997bf4d..594d70ee2 100644
--- a/app/javascript/flavours/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -18,6 +18,8 @@ import {
   COMPOSE_SUGGESTIONS_CLEAR,
   COMPOSE_SUGGESTIONS_READY,
   COMPOSE_SUGGESTION_SELECT,
+  COMPOSE_SUGGESTION_TAGS_UPDATE,
+  COMPOSE_TAG_HISTORY_UPDATE,
   COMPOSE_ADVANCED_OPTIONS_CHANGE,
   COMPOSE_SENSITIVITY_CHANGE,
   COMPOSE_SPOILERNESS_CHANGE,
@@ -39,6 +41,7 @@ import { privacyPreference } from 'flavours/glitch/util/privacy_preference';
 import { me } from 'flavours/glitch/util/initial_state';
 import { overwrite } from 'flavours/glitch/util/js_helpers';
 import { unescapeHTML } from 'flavours/glitch/util/html';
+import { recoverHashtags } from 'flavours/glitch/util/hashtag';
 
 const totalElefriends = 3;
 
@@ -76,6 +79,7 @@ const initialState = ImmutableMap({
   default_sensitive: false,
   resetFileKey: Math.floor((Math.random() * 0x10000)),
   idempotencyKey: null,
+  tagHistory: ImmutableList(),
   doodle: ImmutableMap({
     fg: 'rgb(  0,    0,    0)',
     bg: 'rgb(255,  255,  255)',
@@ -114,8 +118,9 @@ function apiStatusToTextMentions (state, status) {
 }
 
 function apiStatusToTextHashtags (state, status) {
-  return ImmutableOrderedSet([]).union(status.tags.map(
-    ({ name }) => `#${name} `
+  const text = unescapeHTML(status.content);
+  return ImmutableOrderedSet([]).union(recoverHashtags(status.tags, text).map(
+    (name) => `#${name} `
   )).join('');
 }
 
@@ -204,6 +209,18 @@ const insertSuggestion = (state, position, token, completion) => {
   });
 };
 
+const updateSuggestionTags = (state, token) => {
+  const prefix = token.slice(1);
+
+  return state.merge({
+    suggestions: state.get('tagHistory')
+      .filter(tag => tag.toLowerCase().startsWith(prefix.toLowerCase()))
+      .slice(0, 4)
+      .map(tag => '#' + tag),
+    suggestion_token: token,
+  });
+};
+
 const insertEmoji = (state, position, emojiData) => {
   const emoji = emojiData.native;
 
@@ -358,6 +375,10 @@ export default function compose(state = initialState, action) {
     return state.set('suggestions', ImmutableList(action.accounts ? action.accounts.map(item => item.id) : action.emojis)).set('suggestion_token', action.token);
   case COMPOSE_SUGGESTION_SELECT:
     return insertSuggestion(state, action.position, action.token, action.completion);
+  case COMPOSE_SUGGESTION_TAGS_UPDATE:
+    return updateSuggestionTags(state, action.token);
+  case COMPOSE_TAG_HISTORY_UPDATE:
+    return state.set('tagHistory', fromJS(action.tags));
   case TIMELINE_DELETE:
     if (action.id === state.get('in_reply_to')) {
       return state.set('in_reply_to', null);
diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js
index 51032f345..1d24f0e9a 100644
--- a/app/javascript/flavours/glitch/reducers/local_settings.js
+++ b/app/javascript/flavours/glitch/reducers/local_settings.js
@@ -12,6 +12,7 @@ const initialState = ImmutableMap({
   side_arm  : 'none',
   side_arm_reply_mode : 'keep',
   show_reply_count : false,
+  always_show_spoilers_field: false,
   collapsed : ImmutableMap({
     enabled     : true,
     auto        : ImmutableMap({
diff --git a/app/javascript/flavours/glitch/styles/components/metadata.scss b/app/javascript/flavours/glitch/styles/components/metadata.scss
index 29a6330e9..2efe6cd66 100644
--- a/app/javascript/flavours/glitch/styles/components/metadata.scss
+++ b/app/javascript/flavours/glitch/styles/components/metadata.scss
@@ -21,7 +21,7 @@
   dt,
   dd {
     box-sizing: border-box;
-    padding: 14px 20px;
+    padding: 14px 5px;
     text-align: center;
     max-height: 48px;
     overflow: hidden;
diff --git a/app/javascript/flavours/glitch/util/hashtag.js b/app/javascript/flavours/glitch/util/hashtag.js
new file mode 100644
index 000000000..d5ea57662
--- /dev/null
+++ b/app/javascript/flavours/glitch/util/hashtag.js
@@ -0,0 +1,8 @@
+export function recoverHashtags (recognizedTags, text) {
+  return recognizedTags.map(tag => {
+      const re = new RegExp(`(?:^|[^\/\)\w])#(${tag.name})`, 'i');
+      const matched_hashtag = text.match(re);
+      return matched_hashtag ? matched_hashtag[1] : tag;
+    }
+  );
+}
diff --git a/app/javascript/flavours/glitch/util/settings.js b/app/javascript/flavours/glitch/util/settings.js
index dbd969cb1..7643a508e 100644
--- a/app/javascript/flavours/glitch/util/settings.js
+++ b/app/javascript/flavours/glitch/util/settings.js
@@ -44,3 +44,4 @@ export default class Settings {
 }
 
 export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');
+export const tagHistory = new Settings('mastodon_tag_history');