about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/features
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours/glitch/features')
-rw-r--r--app/javascript/flavours/glitch/features/account/components/header.js2
-rw-r--r--app/javascript/flavours/glitch/features/account_gallery/index.js9
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/search_results.js18
-rw-r--r--app/javascript/flavours/glitch/features/follow_recommendations/components/account.js85
-rw-r--r--app/javascript/flavours/glitch/features/follow_recommendations/index.js109
-rw-r--r--app/javascript/flavours/glitch/features/home_timeline/index.js4
-rw-r--r--app/javascript/flavours/glitch/features/notifications/index.js2
-rw-r--r--app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js28
-rw-r--r--app/javascript/flavours/glitch/features/status/components/detailed_status.js4
-rw-r--r--app/javascript/flavours/glitch/features/status/index.js8
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/audio_modal.js32
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/columns_area.js2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/media_modal.js83
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/modal_root.js13
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/video_modal.js29
-rw-r--r--app/javascript/flavours/glitch/features/ui/index.js11
-rw-r--r--app/javascript/flavours/glitch/features/video/index.js27
17 files changed, 355 insertions, 111 deletions
diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js
index c18520c00..4b0494fff 100644
--- a/app/javascript/flavours/glitch/features/account/components/header.js
+++ b/app/javascript/flavours/glitch/features/account/components/header.js
@@ -328,6 +328,8 @@ class Header extends ImmutablePureComponent {
                 )}
 
                 {account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
+
+                <div className='account__header__joined'><FormattedMessage id='account.joined' defaultMessage='Joined {date}' values={{ date: intl.formatDate(account.get('created_at'), { year: 'numeric', month: 'short', day: '2-digit' }) }} /></div>
               </div>
             </div>
           )}
diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js
index 81203e3f8..2a43d1ed2 100644
--- a/app/javascript/flavours/glitch/features/account_gallery/index.js
+++ b/app/javascript/flavours/glitch/features/account_gallery/index.js
@@ -114,15 +114,18 @@ class AccountGallery extends ImmutablePureComponent {
   }
 
   handleOpenMedia = attachment => {
+    const { dispatch } = this.props;
+    const statusId = attachment.getIn(['status', 'id']);
+
     if (attachment.get('type') === 'video') {
-      this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status'), options: { autoPlay: true } }));
+      dispatch(openModal('VIDEO', { media: attachment, statusId, options: { autoPlay: true } }));
     } else if (attachment.get('type') === 'audio') {
-      this.props.dispatch(openModal('AUDIO', { media: attachment, status: attachment.get('status'), options: { autoPlay: true } }));
+      dispatch(openModal('AUDIO', { media: attachment, statusId, options: { autoPlay: true } }));
     } else {
       const media = attachment.getIn(['status', 'media_attachments']);
       const index = media.findIndex(x => x.get('id') === attachment.get('id'));
 
-      this.props.dispatch(openModal('MEDIA', { media, index, status: attachment.get('status') }));
+      dispatch(openModal('MEDIA', { media, index, statusId }));
     }
   }
 
diff --git a/app/javascript/flavours/glitch/features/compose/components/search_results.js b/app/javascript/flavours/glitch/features/compose/components/search_results.js
index bbf997c1f..a0f86a06a 100644
--- a/app/javascript/flavours/glitch/features/compose/components/search_results.js
+++ b/app/javascript/flavours/glitch/features/compose/components/search_results.js
@@ -33,6 +33,12 @@ class SearchResults extends ImmutablePureComponent {
     }
   }
 
+  componentDidUpdate () {
+    if (this.props.searchTerm === '') {
+      this.props.fetchSuggestions();
+    }
+  }
+
   handleLoadMoreAccounts = () => this.props.expandSearch('accounts');
 
   handleLoadMoreStatuses = () => this.props.expandSearch('statuses');
@@ -42,7 +48,7 @@ class SearchResults extends ImmutablePureComponent {
   render () {
     const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props;
 
-    if (results.isEmpty() && !suggestions.isEmpty()) {
+    if (searchTerm === '' && !suggestions.isEmpty()) {
       return (
         <div className='drawer--results'>
           <div className='trends'>
@@ -51,12 +57,12 @@ class SearchResults extends ImmutablePureComponent {
               <FormattedMessage id='suggestions.header' defaultMessage='You might be interested in…' />
             </div>
 
-            {suggestions && suggestions.map(accountId => (
+            {suggestions && suggestions.map(suggestion => (
               <AccountContainer
-                key={accountId}
-                id={accountId}
-                actionIcon='times'
-                actionTitle={intl.formatMessage(messages.dismissSuggestion)}
+                key={suggestion.get('account')}
+                id={suggestion.get('account')}
+                actionIcon={suggestion.get('source') === 'past_interaction' ? 'times' : null}
+                actionTitle={suggestion.get('source') === 'past_interaction' ? intl.formatMessage(messages.dismissSuggestion) : null}
                 onActionClick={dismissSuggestion}
               />
             ))}
diff --git a/app/javascript/flavours/glitch/features/follow_recommendations/components/account.js b/app/javascript/flavours/glitch/features/follow_recommendations/components/account.js
new file mode 100644
index 000000000..046d03a9b
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/follow_recommendations/components/account.js
@@ -0,0 +1,85 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+import { makeGetAccount } from 'flavours/glitch/selectors';
+import Avatar from 'flavours/glitch/components/avatar';
+import DisplayName from 'flavours/glitch/components/display_name';
+import Permalink from 'flavours/glitch/components/permalink';
+import IconButton from 'flavours/glitch/components/icon_button';
+import { injectIntl, defineMessages } from 'react-intl';
+import { followAccount, unfollowAccount } from 'flavours/glitch/actions/accounts';
+
+const messages = defineMessages({
+  follow: { id: 'account.follow', defaultMessage: 'Follow' },
+  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
+});
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, props) => ({
+    account: getAccount(state, props.id),
+  });
+
+  return mapStateToProps;
+};
+
+const getFirstSentence = str => {
+  const arr = str.split(/(([\.\?!]+\s)|[.。?!\n•])/);
+
+  return arr[0];
+};
+
+export default @connect(makeMapStateToProps)
+@injectIntl
+class Account extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.map.isRequired,
+    intl: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+  };
+
+  handleFollow = () => {
+    const { account, dispatch } = this.props;
+
+    if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
+      dispatch(unfollowAccount(account.get('id')));
+    } else {
+      dispatch(followAccount(account.get('id')));
+    }
+  }
+
+  render () {
+    const { account, intl } = this.props;
+
+    let button;
+
+    if (account.getIn(['relationship', 'following'])) {
+      button = <IconButton icon='check' title={intl.formatMessage(messages.unfollow)} active onClick={this.handleFollow} />;
+    } else {
+      button = <IconButton icon='plus' title={intl.formatMessage(messages.follow)} onClick={this.handleFollow} />;
+    }
+
+    return (
+      <div className='account follow-recommendations-account'>
+        <div className='account__wrapper'>
+          <Permalink className='account__display-name account__display-name--with-note' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
+            <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
+
+            <DisplayName account={account} />
+
+            <div className='account__note'>{getFirstSentence(account.get('note_plain'))}</div>
+          </Permalink>
+
+          <div className='account__relationship'>
+            {button}
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/follow_recommendations/index.js b/app/javascript/flavours/glitch/features/follow_recommendations/index.js
new file mode 100644
index 000000000..8165c39a9
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/follow_recommendations/index.js
@@ -0,0 +1,109 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+import { FormattedMessage } from 'react-intl';
+import { fetchSuggestions } from 'flavours/glitch/actions/suggestions';
+import { changeSetting, saveSettings } from 'flavours/glitch/actions/settings';
+import { requestBrowserPermission } from 'flavours/glitch/actions/notifications';
+import { markAsPartial } from 'flavours/glitch/actions/timelines';
+import Column from 'flavours/glitch/features/ui/components/column';
+import Account from './components/account';
+import Logo from 'flavours/glitch/components/logo';
+import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg';
+import Button from 'flavours/glitch/components/button';
+
+const mapStateToProps = state => ({
+  suggestions: state.getIn(['suggestions', 'items']),
+  isLoading: state.getIn(['suggestions', 'isLoading']),
+});
+
+export default @connect(mapStateToProps)
+class FollowRecommendations extends ImmutablePureComponent {
+
+  static contextTypes = {
+    router: PropTypes.object.isRequired,
+  };
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    suggestions: ImmutablePropTypes.list,
+    isLoading: PropTypes.bool,
+  };
+
+  componentDidMount () {
+    const { dispatch, suggestions } = this.props;
+
+    // Don't re-fetch if we're e.g. navigating backwards to this page,
+    // since we don't want followed accounts to disappear from the list
+
+    if (suggestions.size === 0) {
+      dispatch(fetchSuggestions(true));
+    }
+  }
+
+  componentWillUnmount () {
+    const { dispatch } = this.props;
+
+    // Force the home timeline to be reloaded when the user navigates
+    // to it; if the user is new, it would've been empty before
+
+    dispatch(markAsPartial('home'));
+  }
+
+  handleDone = () => {
+    const { dispatch } = this.props;
+    const { router } = this.context;
+
+    dispatch(requestBrowserPermission((permission) => {
+      if (permission === 'granted') {
+        dispatch(changeSetting(['notifications', 'alerts', 'follow'], true));
+        dispatch(changeSetting(['notifications', 'alerts', 'favourite'], true));
+        dispatch(changeSetting(['notifications', 'alerts', 'reblog'], true));
+        dispatch(changeSetting(['notifications', 'alerts', 'mention'], true));
+        dispatch(changeSetting(['notifications', 'alerts', 'poll'], true));
+        dispatch(changeSetting(['notifications', 'alerts', 'status'], true));
+        dispatch(saveSettings());
+      }
+    }));
+
+    router.history.push('/timelines/home');
+  }
+
+  render () {
+    const { suggestions, isLoading } = this.props;
+
+    return (
+      <Column>
+        <div className='scrollable'>
+          <div className='column-title'>
+            <Logo />
+            <h3><FormattedMessage id='follow_recommendations.heading' defaultMessage="Follow people you'd like to see posts from! Here are some suggestions." /></h3>
+            <p><FormattedMessage id='follow_recommendations.lead' defaultMessage="Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!" /></p>
+          </div>
+
+          {!isLoading && (
+            <React.Fragment>
+              <div className='column-list'>
+                {suggestions.size > 0 ? suggestions.map(suggestion => (
+                  <Account key={suggestion.get('account')} id={suggestion.get('account')} />
+                )) : (
+                  <div className='column-list__empty-message'>
+                    <FormattedMessage id='empty_column.follow_recommendations' defaultMessage='Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.' />
+                  </div>
+                )}
+              </div>
+
+              <div className='column-actions'>
+                <img src={imageGreeting} alt='' className='column-actions__background' />
+                <Button onClick={this.handleDone}><FormattedMessage id='follow_recommendations.done' defaultMessage='Done' /></Button>
+              </div>
+            </React.Fragment>
+          )}
+        </div>
+      </Column>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.js b/app/javascript/flavours/glitch/features/home_timeline/index.js
index cc8e4664c..19551d6b8 100644
--- a/app/javascript/flavours/glitch/features/home_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/home_timeline/index.js
@@ -72,7 +72,7 @@ class HomeTimeline extends React.PureComponent {
   }
 
   componentDidMount () {
-    this.props.dispatch(fetchAnnouncements());
+    setTimeout(() => this.props.dispatch(fetchAnnouncements()), 700);
     this._checkIfReloadNeeded(false, this.props.isPartial);
   }
 
@@ -152,7 +152,7 @@ class HomeTimeline extends React.PureComponent {
           scrollKey={`home_timeline-${columnId}`}
           onLoadMore={this.handleLoadMore}
           timelineId='home'
-          emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Visit {public} or use search to get started and meet other users.' values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />}
+          emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up. {suggestions}' values={{ suggestions: <Link to='/start'><FormattedMessage id='empty_column.home.suggestions' defaultMessage='See some suggestions' /></Link> }} />}
           bindToDocument={!multiColumn}
         />
       </Column>
diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js
index 842e02371..6fc951e37 100644
--- a/app/javascript/flavours/glitch/features/notifications/index.js
+++ b/app/javascript/flavours/glitch/features/notifications/index.js
@@ -224,7 +224,7 @@ class Notifications extends React.PureComponent {
     const { notifCleaning, notifCleaningActive } = this.props;
     const { animatingNCD } = this.state;
     const pinned = !!columnId;
-    const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
+    const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. When other people interact with you, you will see it here." />;
 
     let scrollableContent = null;
 
diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js
index d8989ec61..fcb2df527 100644
--- a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js
+++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js
@@ -23,6 +23,7 @@ const messages = defineMessages({
   favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
   replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
   replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
+  open: { id: 'status.open', defaultMessage: 'Expand this status' },
 });
 
 const makeMapStateToProps = () => {
@@ -52,11 +53,19 @@ class Footer extends ImmutablePureComponent {
     dispatch: PropTypes.func.isRequired,
     askReplyConfirmation: PropTypes.bool,
     showReplyCount: PropTypes.bool,
+    withOpenButton: PropTypes.bool,
+    onClose: PropTypes.func,
   };
 
   _performReply = () => {
-    const { dispatch, status } = this.props;
-    dispatch(replyCompose(status, this.context.router.history));
+    const { dispatch, status, onClose } = this.props;
+    const { router } = this.context;
+
+    if (onClose) {
+      onClose();
+    }
+
+    dispatch(replyCompose(status, router.history));
   };
 
   handleReplyClick = () => {
@@ -100,8 +109,20 @@ class Footer extends ImmutablePureComponent {
     }
   };
 
+  handleOpenClick = e => {
+    const { router } = this.context;
+
+    if (e.button !== 0 || !router) {
+      return;
+    }
+
+    const { status } = this.props;
+
+    router.history.push(`/statuses/${status.get('id')}`);
+  }
+
   render () {
-    const { status, intl, showReplyCount } = this.props;
+    const { status, intl, showReplyCount, withOpenButton } = this.props;
 
     const publicStatus  = ['public', 'unlisted'].includes(status.get('visibility'));
     const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
@@ -156,6 +177,7 @@ class Footer extends ImmutablePureComponent {
         {replyButton}
         <IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate}  active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
         <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
+        {withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' onClick={this.handleOpenClick} />}
       </div>
     );
   }
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 40bf370f3..4cc1d1af5 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -68,8 +68,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
     e.stopPropagation();
   }
 
-  handleOpenVideo = (media, options) => {
-    this.props.onOpenVideo(media, options);
+  handleOpenVideo = (options) => {
+    this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options);
   }
 
   _measureHeight (heightJustChanged) {
diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index 21e441407..513a6227f 100644
--- a/app/javascript/flavours/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -316,11 +316,11 @@ class Status extends ImmutablePureComponent {
   }
 
   handleOpenMedia = (media, index) => {
-    this.props.dispatch(openModal('MEDIA', { media, index }));
+    this.props.dispatch(openModal('MEDIA', { statusId: this.props.status.get('id'), media, index }));
   }
 
   handleOpenVideo = (media, options) => {
-    this.props.dispatch(openModal('VIDEO', { media, options }));
+    this.props.dispatch(openModal('VIDEO', { statusId: this.props.status.get('id'), media, options }));
   }
 
   handleHotkeyOpenMedia = e => {
@@ -329,9 +329,7 @@ class Status extends ImmutablePureComponent {
     e.preventDefault();
 
     if (status.get('media_attachments').size > 0) {
-      if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
-        // TODO: toggle play/paused?
-      } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
+      if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
         this.handleOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
       } else {
         this.handleOpenMedia(status.get('media_attachments'), 0);
diff --git a/app/javascript/flavours/glitch/features/ui/components/audio_modal.js b/app/javascript/flavours/glitch/features/ui/components/audio_modal.js
index f9d4bb2f3..fc98cc6af 100644
--- a/app/javascript/flavours/glitch/features/ui/components/audio_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/audio_modal.js
@@ -4,12 +4,10 @@ import PropTypes from 'prop-types';
 import Audio from 'flavours/glitch/features/audio';
 import { connect } from 'react-redux';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { FormattedMessage } from 'react-intl';
-import classNames from 'classnames';
-import Icon from 'flavours/glitch/components/icon';
+import Footer from 'flavours/glitch/features/picture_in_picture/components/footer';
 
-const mapStateToProps = (state, { status }) => ({
-  account: state.getIn(['accounts', status.get('account')]),
+const mapStateToProps = (state, { statusId }) => ({
+  accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']),
 });
 
 export default @connect(mapStateToProps)
@@ -17,27 +15,21 @@ class AudioModal extends ImmutablePureComponent {
 
   static propTypes = {
     media: ImmutablePropTypes.map.isRequired,
-    status: ImmutablePropTypes.map,
+    statusId: PropTypes.string.isRequired,
+    accountStaticAvatar: PropTypes.string.isRequired,
     options: PropTypes.shape({
       autoPlay: PropTypes.bool,
     }),
-    account: ImmutablePropTypes.map,
     onClose: PropTypes.func.isRequired,
+    onChangeBackgroundColor: PropTypes.func.isRequired,
   };
 
   static contextTypes = {
     router: PropTypes.object,
   };
 
-  handleStatusClick = e => {
-    if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
-      e.preventDefault();
-      this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
-    }
-  }
-
   render () {
-    const { media, status, account } = this.props;
+    const { media, accountStaticAvatar, statusId, onClose } = this.props;
     const options = this.props.options || {};
 
     return (
@@ -48,7 +40,7 @@ class AudioModal extends ImmutablePureComponent {
             alt={media.get('description')}
             duration={media.getIn(['meta', 'original', 'duration'], 0)}
             height={150}
-            poster={media.get('preview_url') || account.get('avatar_static')}
+            poster={media.get('preview_url') || accountStaticAvatar}
             backgroundColor={media.getIn(['meta', 'colors', 'background'])}
             foregroundColor={media.getIn(['meta', 'colors', 'foreground'])}
             accentColor={media.getIn(['meta', 'colors', 'accent'])}
@@ -56,11 +48,9 @@ class AudioModal extends ImmutablePureComponent {
           />
         </div>
 
-        {status && (
-          <div className={classNames('media-modal__meta')}>
-            <a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
-          </div>
-        )}
+        <div className='media-modal__overlay'>
+          {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
+        </div>
       </div>
     );
   }
diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js
index 4ea7b48fe..d4e0bedac 100644
--- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js
+++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js
@@ -47,7 +47,7 @@ const componentMap = {
   'DIRECTORY': Directory,
 };
 
-const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started/);
+const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started|^\/start/);
 
 const messages = defineMessages({
   publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
diff --git a/app/javascript/flavours/glitch/features/ui/components/media_modal.js b/app/javascript/flavours/glitch/features/ui/components/media_modal.js
index e37df7208..a8cbb837e 100644
--- a/app/javascript/flavours/glitch/features/ui/components/media_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.js
@@ -4,12 +4,14 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import Video from 'flavours/glitch/features/video';
 import classNames from 'classnames';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { defineMessages, injectIntl } from 'react-intl';
 import IconButton from 'flavours/glitch/components/icon_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import ImageLoader from './image_loader';
 import Icon from 'flavours/glitch/components/icon';
 import GIFV from 'flavours/glitch/components/gifv';
+import Footer from 'flavours/glitch/features/picture_in_picture/components/footer';
+import { getAverageFromBlurhash } from 'flavours/glitch/blurhash';
 
 const messages = defineMessages({
   close: { id: 'lightbox.close', defaultMessage: 'Close' },
@@ -26,10 +28,14 @@ class MediaModal extends ImmutablePureComponent {
 
   static propTypes = {
     media: ImmutablePropTypes.list.isRequired,
-    status: ImmutablePropTypes.map,
+    statusId: PropTypes.string,
     index: PropTypes.number.isRequired,
     onClose: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
+    onChangeBackgroundColor: PropTypes.func.isRequired,
+    currentTime: PropTypes.number,
+    autoPlay: PropTypes.bool,
+    volume: PropTypes.number,
   };
 
   state = {
@@ -64,6 +70,7 @@ class MediaModal extends ImmutablePureComponent {
 
   handleChangeIndex = (e) => {
     const index = Number(e.currentTarget.getAttribute('data-index'));
+
     this.setState({
       index: index % this.props.media.size,
       zoomButtonHidden: true,
@@ -87,10 +94,12 @@ class MediaModal extends ImmutablePureComponent {
 
   componentDidMount () {
     window.addEventListener('keydown', this.handleKeyDown, false);
+    this._sendBackgroundColor();
   }
 
   componentWillUnmount () {
     window.removeEventListener('keydown', this.handleKeyDown);
+    this.props.onChangeBackgroundColor(null);
   }
 
   getIndex () {
@@ -106,30 +115,38 @@ class MediaModal extends ImmutablePureComponent {
   handleStatusClick = e => {
     if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
       e.preventDefault();
-      this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
+      this.context.router.history.push(`/statuses/${this.props.statusId}`);
+    }
+
+    this._sendBackgroundColor();
+  }
+
+  componentDidUpdate (prevProps, prevState) {
+    if (prevState.index !== this.state.index) {
+      this._sendBackgroundColor();
+    }
+  }
+
+  _sendBackgroundColor () {
+    const { media, onChangeBackgroundColor } = this.props;
+    const index = this.getIndex();
+    const blurhash = media.getIn([index, 'blurhash']);
+
+    if (blurhash) {
+      const backgroundColor = getAverageFromBlurhash(blurhash);
+      onChangeBackgroundColor(backgroundColor);
     }
   }
 
   render () {
-    const { media, status, intl, onClose } = this.props;
+    const { media, statusId, intl, onClose } = this.props;
     const { navigationHidden } = this.state;
 
     const index = this.getIndex();
-    let pagination = [];
 
     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>;
     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>;
 
-    if (media.size > 1) {
-      pagination = media.map((item, i) => {
-        const classes = ['media-modal__button'];
-        if (i === index) {
-          classes.push('media-modal__button--active');
-        }
-        return (<li className='media-modal__page-dot' key={i}><button tabIndex='0' className={classes.join(' ')} onClick={this.handleChangeIndex} data-index={i}>{i + 1}</button></li>);
-      });
-    }
-
     const content = media.map((image) => {
       const width  = image.getIn(['meta', 'original', 'width']) || null;
       const height = image.getIn(['meta', 'original', 'height']) || null;
@@ -148,7 +165,7 @@ class MediaModal extends ImmutablePureComponent {
           />
         );
       } else if (image.get('type') === 'video') {
-        const { time } = this.props;
+        const { currentTime, autoPlay, volume } = this.props;
 
         return (
           <Video
@@ -157,7 +174,10 @@ class MediaModal extends ImmutablePureComponent {
             src={image.get('url')}
             width={image.get('width')}
             height={image.get('height')}
-            currentTime={time || 0}
+            frameRate={image.getIn(['meta', 'original', 'frame_rate'])}
+            currentTime={currentTime || 0}
+            autoPlay={autoPlay || false}
+            volume={volume || 1}
             onCloseVideo={onClose}
             detailed
             alt={image.get('description')}
@@ -197,13 +217,19 @@ class MediaModal extends ImmutablePureComponent {
       'media-modal__navigation--hidden': navigationHidden,
     });
 
+    let pagination;
+
+    if (media.size > 1) {
+      pagination = media.map((item, i) => (
+        <button key={i} className={classNames('media-modal__page-dot', { active: i === index })} data-index={i} onClick={this.handleChangeIndex}>
+          {i + 1}
+        </button>
+      ));
+    }
+
     return (
       <div className='modal-root__modal media-modal'>
-        <div
-          className='media-modal__closer'
-          role='presentation'
-          onClick={onClose}
-        >
+        <div className='media-modal__closer' role='presentation' onClick={onClose} >
           <ReactSwipeableViews
             style={swipeableViewsStyle}
             containerStyle={containerStyle}
@@ -221,15 +247,10 @@ class MediaModal extends ImmutablePureComponent {
           {leftNav}
           {rightNav}
 
-          {status && (
-            <div className={classNames('media-modal__meta', { 'media-modal__meta--shifted': media.size > 1 })}>
-              <a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
-            </div>
-          )}
-
-          <ul className='media-modal__pagination'>
-            {pagination}
-          </ul>
+          <div className='media-modal__overlay'>
+            {pagination && <ul className='media-modal__pagination'>{pagination}</ul>}
+            {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
+          </div>
         </div>
       </div>
     );
diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
index 488daf0cc..0fd70de34 100644
--- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js
+++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
@@ -55,6 +55,10 @@ export default class ModalRoot extends React.PureComponent {
     onClose: PropTypes.func.isRequired,
   };
 
+  state = {
+    backgroundColor: null,
+  };
+
   getSnapshotBeforeUpdate () {
     return { visible: !!this.props.type };
   }
@@ -69,6 +73,10 @@ export default class ModalRoot extends React.PureComponent {
     }
   }
 
+  setBackgroundColor = color => {
+    this.setState({ backgroundColor: color });
+  }
+
   renderLoading = modalId => () => {
     return ['MEDIA', 'VIDEO', 'BOOST', 'FAVOURITE', 'DOODLE', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
   }
@@ -81,13 +89,14 @@ export default class ModalRoot extends React.PureComponent {
 
   render () {
     const { type, props, onClose } = this.props;
+    const { backgroundColor } = this.state;
     const visible = !!type;
 
     return (
-      <Base onClose={onClose} noEsc={props ? props.noEsc : false}>
+      <Base backgroundColor={backgroundColor} onClose={onClose} noEsc={props ? props.noEsc : false}>
         {visible && (
           <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
-            {(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />}
+            {(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={onClose} />}
           </BundleContainer>
         )}
       </Base>
diff --git a/app/javascript/flavours/glitch/features/ui/components/video_modal.js b/app/javascript/flavours/glitch/features/ui/components/video_modal.js
index b0a4f3f03..6b6e615a6 100644
--- a/app/javascript/flavours/glitch/features/ui/components/video_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/video_modal.js
@@ -3,9 +3,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import Video from 'flavours/glitch/features/video';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { FormattedMessage } from 'react-intl';
-import classNames from 'classnames';
-import Icon from 'flavours/glitch/components/icon';
+import Footer from 'flavours/glitch/features/picture_in_picture/components/footer';
+import { getAverageFromBlurhash } from 'flavours/glitch/blurhash';
 
 export default class VideoModal extends ImmutablePureComponent {
 
@@ -15,24 +14,28 @@ export default class VideoModal extends ImmutablePureComponent {
 
   static propTypes = {
     media: ImmutablePropTypes.map.isRequired,
-    status: ImmutablePropTypes.map,
+    statusId: PropTypes.string,
     options: PropTypes.shape({
       startTime: PropTypes.number,
       autoPlay: PropTypes.bool,
       defaultVolume: PropTypes.number,
     }),
     onClose: PropTypes.func.isRequired,
+    onChangeBackgroundColor: PropTypes.func.isRequired,
   };
 
-  handleStatusClick = e => {
-    if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
-      e.preventDefault();
-      this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
+  componentDidMount () {
+    const { media, onChangeBackgroundColor, onClose } = this.props;
+
+    const backgroundColor = getAverageFromBlurhash(media.get('blurhash'));
+
+    if (backgroundColor) {
+      onChangeBackgroundColor(backgroundColor);
     }
   }
 
   render () {
-    const { media, status, onClose } = this.props;
+    const { media, statusId, onClose } = this.props;
     const options = this.props.options || {};
 
     return (
@@ -52,11 +55,9 @@ export default class VideoModal extends ImmutablePureComponent {
           />
         </div>
 
-        {status && (
-          <div className={classNames('media-modal__meta')}>
-            <a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
-          </div>
-        )}
+        <div className='media-modal__overlay'>
+          {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
+        </div>
       </div>
     );
   }
diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js
index 61a34fd2b..1149eb14e 100644
--- a/app/javascript/flavours/glitch/features/ui/index.js
+++ b/app/javascript/flavours/glitch/features/ui/index.js
@@ -50,9 +50,11 @@ import {
   Search,
   GettingStartedMisc,
   Directory,
+  FollowRecommendations,
 } from 'flavours/glitch/util/async-components';
 import { HotKeys } from 'react-hotkeys';
 import { me } from 'flavours/glitch/util/initial_state';
+import { closeOnboarding, INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding';
 import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
 
 // Dummy import, to make sure that <Status /> ends up in the application bundle.
@@ -75,6 +77,7 @@ const mapStateToProps = state => ({
   showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']),
   hicolorPrivacyIcons: state.getIn(['local_settings', 'hicolor_privacy_icons']),
   moved: state.getIn(['accounts', me, 'moved']) && state.getIn(['accounts', state.getIn(['accounts', me, 'moved'])]),
+  firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
 });
 
 const keyMap = {
@@ -207,6 +210,7 @@ class SwitchingColumnsArea extends React.PureComponent {
           <WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
           <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
 
+          <WrappedRoute path='/start' component={FollowRecommendations} content={children} />
           <WrappedRoute path='/search' component={Search} content={children} />
           <WrappedRoute path='/directory' component={Directory} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
 
@@ -260,6 +264,7 @@ class UI extends React.Component {
     unreadNotifications: PropTypes.number,
     showFaviconBadge: PropTypes.bool,
     moved: PropTypes.map,
+    firstLaunch: PropTypes.bool,
   };
 
   state = {
@@ -378,6 +383,12 @@ class UI extends React.Component {
 
     this.favicon = new Favico({ animation:"none" });
 
+    // On first launch, redirect to the follow recommendations page
+    if (this.props.firstLaunch) {
+      this.context.router.history.replace('/start');
+      this.props.dispatch(closeOnboarding());
+    }
+
     this.props.dispatch(fetchMarkers());
     this.props.dispatch(expandHomeTimeline());
     this.props.dispatch(expandNotifications());
diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js
index a81311c67..fcbf07ce2 100644
--- a/app/javascript/flavours/glitch/features/video/index.js
+++ b/app/javascript/flavours/glitch/features/video/index.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { fromJS, is } from 'immutable';
+import { is } from 'immutable';
 import { throttle, debounce } from 'lodash';
 import classNames from 'classnames';
 import { isFullscreen, requestFullscreen, exitFullscreen } from 'flavours/glitch/util/fullscreen';
@@ -120,10 +120,10 @@ class Video extends React.PureComponent {
     deployPictureInPicture: PropTypes.func,
     preventPlayback: PropTypes.bool,
     blurhash: PropTypes.string,
-    link: PropTypes.node,
     autoPlay: PropTypes.bool,
     volume: PropTypes.number,
     muted: PropTypes.bool,
+    componetIndex: PropTypes.number,
   };
 
   static defaultProps = {
@@ -510,25 +510,14 @@ class Video extends React.PureComponent {
   }
 
   handleOpenVideo = () => {
-    const { src, preview, width, height, alt } = this.props;
-
-    const media = fromJS({
-      type: 'video',
-      url: src,
-      preview_url: preview,
-      description: alt,
-      width,
-      height,
-    });
+    this.video.pause();
 
-    const options = {
+    this.props.onOpenVideo({
       startTime: this.video.currentTime,
       autoPlay: !this.state.paused,
       defaultVolume: this.state.volume,
-    };
-
-    this.video.pause();
-    this.props.onOpenVideo(media, options);
+      componetIndex: this.props.componetIndex,
+    });
   }
 
   handleCloseVideo = () => {
@@ -548,7 +537,7 @@ class Video extends React.PureComponent {
   }
 
   render () {
-    const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, link, editable, blurhash } = this.props;
+    const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, editable, blurhash } = this.props;
     const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
     const progress = Math.min((currentTime / duration) * 100, 100);
     const playerStyle = {};
@@ -666,8 +655,6 @@ class Video extends React.PureComponent {
                   <span className='video-player__time-total'>{formatTime(Math.floor(duration))}</span>
                 </span>
               )}
-
-              {link && <span className='video-player__link'>{link}</span>}
             </div>
 
             <div className='video-player__buttons right'>