diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/controllers/admin/instances_controller.rb | 11 | ||||
-rw-r--r-- | app/helpers/emoji_helper.rb | 15 | ||||
-rw-r--r-- | app/javascript/mastodon/actions/compose.js | 8 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/navigation_bar.js | 5 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/index.js | 13 | ||||
-rw-r--r-- | app/javascript/mastodon/features/ui/index.js | 15 | ||||
-rw-r--r-- | app/javascript/mastodon/reducers/compose.js | 8 | ||||
-rw-r--r-- | app/javascript/packs/public.js | 14 | ||||
-rw-r--r-- | app/javascript/styles/accounts.scss | 13 | ||||
-rw-r--r-- | app/javascript/styles/components.scss | 68 | ||||
-rw-r--r-- | app/javascript/styles/forms.scss | 4 | ||||
-rw-r--r-- | app/lib/emoji.rb | 40 | ||||
-rw-r--r-- | app/views/admin/instances/_instance.html.haml | 2 | ||||
-rw-r--r-- | app/views/settings/profiles/show.html.haml | 9 |
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 |