about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/admin/instances_controller.rb11
-rw-r--r--app/helpers/emoji_helper.rb15
-rw-r--r--app/javascript/mastodon/actions/compose.js8
-rw-r--r--app/javascript/mastodon/features/compose/components/navigation_bar.js5
-rw-r--r--app/javascript/mastodon/features/compose/index.js13
-rw-r--r--app/javascript/mastodon/features/ui/index.js15
-rw-r--r--app/javascript/mastodon/reducers/compose.js8
-rw-r--r--app/javascript/packs/public.js14
-rw-r--r--app/javascript/styles/accounts.scss13
-rw-r--r--app/javascript/styles/components.scss68
-rw-r--r--app/javascript/styles/forms.scss4
-rw-r--r--app/lib/emoji.rb40
-rw-r--r--app/views/admin/instances/_instance.html.haml2
-rw-r--r--app/views/settings/profiles/show.html.haml9
14 files changed, 216 insertions, 9 deletions
diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb
index ac93248a8..3296e08db 100644
--- a/app/controllers/admin/instances_controller.rb
+++ b/app/controllers/admin/instances_controller.rb
@@ -6,15 +6,26 @@ module Admin
       @instances = ordered_instances
     end
 
+    def resubscribe
+      params.require(:by_domain)
+      Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id))
+      redirect_to admin_instances_path
+    end
+
     private
 
     def paginated_instances
       Account.remote.by_domain_accounts.page(params[:page])
     end
+
     helper_method :paginated_instances
 
     def ordered_instances
       paginated_instances.map { |account| Instance.new(account) }
     end
+
+    def subscribeable_accounts
+      Account.with_followers.remote.where(domain: params[:by_domain])
+    end
   end
 end
diff --git a/app/helpers/emoji_helper.rb b/app/helpers/emoji_helper.rb
index c1595851f..848c03fce 100644
--- a/app/helpers/emoji_helper.rb
+++ b/app/helpers/emoji_helper.rb
@@ -1,19 +1,24 @@
 # frozen_string_literal: true
 
 module EmojiHelper
-  EMOJI_PATTERN = /(?<=[^[:alnum:]:]|\n|^):([\w+-]+):(?=[^[:alnum:]:]|$)/x
-
   def emojify(text)
     return text if text.blank?
 
-    text.gsub(EMOJI_PATTERN) do |match|
-      emoji = Emoji.find_by_alias($1) # rubocop:disable Rails/DynamicFindBy,Style/PerlBackrefs
+    text.gsub(emoji_pattern) do |match|
+      emoji = Emoji.instance.unicode($1) # rubocop:disable Style/PerlBackrefs
 
       if emoji
-        emoji.raw
+        emoji
       else
         match
       end
     end
   end
+
+  def emoji_pattern
+    @emoji_pattern ||=
+      /(?<=[^[:alnum:]:]|\n|^)
+      (#{Emoji.instance.names.map { |name| Regexp.escape(name) }.join('|')})
+      (?=[^[:alnum:]:]|$)/x
+  end
 end
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 2ce4e9b4e..5a486f9bb 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -28,6 +28,7 @@ export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
 export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
 export const COMPOSE_VISIBILITY_CHANGE  = 'COMPOSE_VISIBILITY_CHANGE';
 export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
+export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
 
 export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
 
@@ -288,3 +289,10 @@ export function insertEmojiCompose(position, emoji) {
     emoji,
   };
 };
+
+export function changeComposing(value) {
+  return {
+    type: COMPOSE_COMPOSING_CHANGE,
+    value,
+  };
+}
diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.js b/app/javascript/mastodon/features/compose/components/navigation_bar.js
index 7983edb85..b0bc0958e 100644
--- a/app/javascript/mastodon/features/compose/components/navigation_bar.js
+++ b/app/javascript/mastodon/features/compose/components/navigation_bar.js
@@ -1,6 +1,8 @@
 import React from 'react';
+import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import Avatar from '../../../components/avatar';
+import IconButton from '../../../components/icon_button';
 import Permalink from '../../../components/permalink';
 import { FormattedMessage } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -9,6 +11,7 @@ export default class NavigationBar extends ImmutablePureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map.isRequired,
+    onClose: PropTypes.func.isRequired,
   };
 
   render () {
@@ -25,6 +28,8 @@ export default class NavigationBar extends ImmutablePureComponent {
 
           <a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
         </div>
+
+        <IconButton title='' icon='close' onClick={this.props.onClose} />
       </div>
     );
   }
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index 69bead689..66b0746c5 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -13,6 +13,7 @@ import SearchContainer from './containers/search_container';
 import Motion from 'react-motion/lib/Motion';
 import spring from 'react-motion/lib/spring';
 import SearchResultsContainer from './containers/search_results_container';
+import { changeComposing } from '../../actions/compose';
 
 const messages = defineMessages({
   start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@@ -59,6 +60,14 @@ export default class Compose extends React.PureComponent {
     this.props.dispatch(openModal('SETTINGS', {}));
   }
 
+  onFocus = () => {
+    this.props.dispatch(changeComposing(true));
+  }
+
+  onBlur = () => {
+    this.props.dispatch(changeComposing(false));
+  }
+
   render () {
     const { multiColumn, showSearch, intl } = this.props;
 
@@ -96,8 +105,8 @@ export default class Compose extends React.PureComponent {
         <SearchContainer />
 
         <div className='drawer__pager'>
-          <div className='drawer__inner'>
-            <NavigationContainer />
+          <div className='drawer__inner' onFocus={this.onFocus}>
+            <NavigationContainer onClose={this.onBlur} />
             <ComposeFormContainer />
           </div>
 
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 5a0398eb4..1edc7504c 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -45,6 +45,7 @@ const mapStateToProps = state => ({
   systemFontUi: state.getIn(['meta', 'system_font_ui']),
   layout: state.getIn(['local_settings', 'layout']),
   isWide: state.getIn(['local_settings', 'stretch']),
+  isComposing: state.getIn(['compose', 'is_composing']),
 });
 
 @connect(mapStateToProps)
@@ -56,6 +57,7 @@ export default class UI extends React.PureComponent {
     layout: PropTypes.string,
     isWide: PropTypes.bool,
     systemFontUi: PropTypes.bool,
+    isComposing: PropTypes.bool,
   };
 
   state = {
@@ -137,6 +139,19 @@ export default class UI extends React.PureComponent {
     this.props.dispatch(refreshNotifications());
   }
 
+  shouldComponentUpdate (nextProps) {
+    if (nextProps.isComposing !== this.props.isComposing) {
+      // Avoid expensive update just to toggle a class
+      this.node.classList.toggle('is-composing', nextProps.isComposing);
+
+      return false;
+    }
+
+    // Why isn't this working?!?
+    // return super.shouldComponentUpdate(nextProps, nextState);
+    return true;
+  }
+
   componentWillUnmount () {
     window.removeEventListener('resize', this.handleResize);
     document.removeEventListener('dragenter', this.handleDragEnter);
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 4dce634a4..7c98854a2 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -21,6 +21,7 @@ import {
   COMPOSE_SPOILERNESS_CHANGE,
   COMPOSE_SPOILER_TEXT_CHANGE,
   COMPOSE_VISIBILITY_CHANGE,
+  COMPOSE_COMPOSING_CHANGE,
   COMPOSE_EMOJI_INSERT,
 } from '../actions/compose';
 import { TIMELINE_DELETE } from '../actions/timelines';
@@ -41,6 +42,7 @@ const initialState = ImmutableMap({
   focusDate: null,
   preselectDate: null,
   in_reply_to: null,
+  is_composing: false,
   is_submitting: false,
   is_uploading: false,
   progress: 0,
@@ -154,7 +156,9 @@ export default function compose(state = initialState, action) {
   case COMPOSE_MOUNT:
     return state.set('mounted', true);
   case COMPOSE_UNMOUNT:
-    return state.set('mounted', false);
+    return state
+      .set('mounted', false)
+      .set('is_composing', false);
   case COMPOSE_ADVANCED_OPTIONS_CHANGE:
     return state
       .set('advanced_options',
@@ -182,6 +186,8 @@ export default function compose(state = initialState, action) {
     return state
       .set('text', action.text)
       .set('idempotencyKey', uuid());
+  case COMPOSE_COMPOSING_CHANGE:
+    return state.set('is_composing', action.value);
   case COMPOSE_REPLY:
     return state.withMutations(map => {
       map.set('in_reply_to', action.status.get('id'));
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index e34c47fd0..e9bb4a42e 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -90,6 +90,20 @@ function main() {
       noteCounter.textContent = 500 - length(noteWithoutMetadata);
     }
   });
+
+  delegate(document, '#account_avatar', 'change', ({ target }) => {
+    const avatar = document.querySelector('.card.compact .avatar img');
+    const [file] = target.files || [];
+    const url = URL.createObjectURL(file);
+    avatar.src = url;
+  });
+
+  delegate(document, '#account_header', 'change', ({ target }) => {
+    const header = document.querySelector('.card.compact');
+    const [file] = target.files || [];
+    const url = URL.createObjectURL(file);
+    header.style.backgroundImage = `url(${url})`;
+  });
 }
 
 loadPolyfills().then(main).catch(error => {
diff --git a/app/javascript/styles/accounts.scss b/app/javascript/styles/accounts.scss
index 95b097f41..3d5c1a692 100644
--- a/app/javascript/styles/accounts.scss
+++ b/app/javascript/styles/accounts.scss
@@ -31,6 +31,19 @@
     }
   }
 
+  &.compact {
+    padding: 30px 0;
+    border-radius: 4px;
+
+    .avatar {
+      margin-bottom: 0;
+
+      img {
+        object-fit: cover;
+      }
+    }
+  }
+
   .name {
     display: block;
     position: relative;
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss
index 3e80569a9..d67e2ca69 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/components.scss
@@ -1464,6 +1464,11 @@
   .permalink {
     text-decoration: none;
   }
+
+  .icon-button {
+    pointer-events: none;
+    opacity: 0;
+  }
 }
 
 .navigation-bar__profile {
@@ -4160,3 +4165,66 @@ noscript {
     margin: 20px 0;
   }
 }
+
+@media screen and (max-width: 1024px) and (max-height: 400px) {
+  $duration: 400ms;
+  $delay: 100ms;
+
+  .tabs-bar,
+  .search {
+    will-change: margin-top;
+    transition: margin-top $duration $delay;
+  }
+
+  .navigation-bar {
+    will-change: padding-bottom;
+    transition: padding-bottom $duration $delay;
+  }
+
+  .navigation-bar {
+    & > a:first-child {
+      will-change: margin-top, margin-left, width;
+      transition: margin-top $duration $delay, margin-left $duration ($duration + $delay);
+    }
+
+    & > .navigation-bar__profile-edit {
+      will-change: margin-top;
+      transition: margin-top $duration $delay;
+    }
+
+    & > .icon-button {
+      will-change: opacity;
+      transition: opacity $duration $delay;
+    }
+  }
+
+  .is-composing {
+    .tabs-bar,
+    .search {
+      margin-top: -50px;
+    }
+
+    .navigation-bar {
+      padding-bottom: 0;
+
+      & > a:first-child {
+        margin-top: -50px;
+        margin-left: -40px;
+      }
+
+      .navigation-bar__profile {
+        padding-top: 2px;
+      }
+
+      .navigation-bar__profile-edit {
+        position: absolute;
+        margin-top: -50px;
+      }
+
+      .icon-button {
+        pointer-events: auto;
+        opacity: 1;
+      }
+    }
+  }
+}
diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/forms.scss
index e1de36d55..c467aa7db 100644
--- a/app/javascript/styles/forms.scss
+++ b/app/javascript/styles/forms.scss
@@ -40,6 +40,10 @@ code {
     }
   }
 
+  .card {
+    margin-bottom: 15px;
+  }
+
   strong {
     font-weight: 500;
   }
diff --git a/app/lib/emoji.rb b/app/lib/emoji.rb
new file mode 100644
index 000000000..e444b6893
--- /dev/null
+++ b/app/lib/emoji.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'singleton'
+
+class Emoji
+  include Singleton
+
+  def initialize
+    data = Oj.load(File.open(File.join(Rails.root, 'lib', 'assets', 'emoji.json')))
+
+    @map = {}
+
+    data.each do |_, emoji|
+      keys    = [emoji['shortname']] + emoji['aliases']
+      unicode = codepoint_to_unicode(emoji['unicode'])
+
+      keys.each do |key|
+        @map[key] = unicode
+      end
+    end
+  end
+
+  def unicode(shortcode)
+    @map[shortcode]
+  end
+
+  def names
+    @map.keys
+  end
+
+  private
+
+  def codepoint_to_unicode(codepoint)
+    if codepoint.include?('-')
+      codepoint.split('-').map(&:hex).pack('U')
+    else
+      [codepoint.hex].pack('U')
+    end
+  end
+end
diff --git a/app/views/admin/instances/_instance.html.haml b/app/views/admin/instances/_instance.html.haml
index 5c6783d06..435cd8f64 100644
--- a/app/views/admin/instances/_instance.html.haml
+++ b/app/views/admin/instances/_instance.html.haml
@@ -3,3 +3,5 @@
     = instance.domain
   %td.count
     = instance.accounts_count
+  %td
+    = table_link_to 'paper-plane-o', t('admin.accounts.resubscribe'), resubscribe_admin_instances_url(by_domain: instance.domain), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }
diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml
index 8dc61fec9..3fa540bba 100644
--- a/app/views/settings/profiles/show.html.haml
+++ b/app/views/settings/profiles/show.html.haml
@@ -7,10 +7,17 @@
   .fields-group
     = f.input :display_name, placeholder: t('simple_form.labels.defaults.display_name'), hint: t('simple_form.hints.defaults.display_name', count: 30 - @account.display_name.size).html_safe
     = f.input :note, placeholder: t('simple_form.labels.defaults.note'), hint: t('simple_form.hints.defaults.note', count: 500 - @account.note.size).html_safe
+
+  .card.compact{ style: "background-image: url(#{@account.header.url(:original)})" }
+    .avatar= image_tag @account.avatar.url(:original)
+
+  .fields-group
     = f.input :avatar, wrapper: :with_label, input_html: { accept: AccountAvatar::IMAGE_MIME_TYPES.join(',') }, hint: t('simple_form.hints.defaults.avatar')
+
     = f.input :header, wrapper: :with_label, input_html: { accept: AccountHeader::IMAGE_MIME_TYPES.join(',') }, hint: t('simple_form.hints.defaults.header')
 
-  = f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked')
+  .fields-group
+    = f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked')
 
   .actions
     = f.button :button, t('generic.save_changes'), type: :submit