diff --git a/.codeclimate.yml b/.codeclimate.yml index 21e6b33bf..58f6b3de4 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -30,6 +30,7 @@ plugins: channel: eslint-4 rubocop: enabled: true + channel: rubocop-0-54 scss-lint: enabled: true exclude_patterns: diff --git a/.env.production.sample b/.env.production.sample index 24b6b0143..3047f7595 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -88,6 +88,10 @@ SMTP_FROM_ADDRESS=notifications@example.com # CDN_HOST=https://assets.example.com # S3 (optional) +# The attachment host must allow cross origin request from WEB_DOMAIN or +# LOCAL_DOMAIN if WEB_DOMAIN is not set. For example, the server may have the +# following header field: +# Access-Control-Allow-Origin: https://192.168.1.123:9000/ # S3_ENABLED=true # S3_BUCKET= # AWS_ACCESS_KEY_ID= @@ -97,6 +101,8 @@ SMTP_FROM_ADDRESS=notifications@example.com # S3_HOSTNAME=192.168.1.123:9000 # S3 (Minio Config (optional) Please check Minio instance for details) +# The attachment host must allow cross origin request - see the description +# above. # S3_ENABLED=true # S3_BUCKET= # AWS_ACCESS_KEY_ID= @@ -108,6 +114,8 @@ SMTP_FROM_ADDRESS=notifications@example.com # S3_SIGNATURE_VERSION= # Swift (optional) +# The attachment host must allow cross origin request - see the description +# above. # SWIFT_ENABLED=true # SWIFT_USERNAME= # For Keystone V3, the value for SWIFT_TENANT should be the project name diff --git a/.eslintrc.yml b/.eslintrc.yml index 576e5b70a..fbda26549 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -7,6 +7,9 @@ env: es6: true jest: true +globals: + ATTACHMENT_HOST: false + parser: babel-eslint plugins: @@ -110,13 +113,23 @@ rules: jsx-a11y/accessible-emoji: warn jsx-a11y/alt-text: warn jsx-a11y/anchor-has-content: warn + jsx-a11y/anchor-is-valid: + - warn + - components: + - Link + - NavLink + specialLink: + - to + aspect: + - noHref + - invalidHref + - preferButton jsx-a11y/aria-activedescendant-has-tabindex: warn jsx-a11y/aria-props: warn jsx-a11y/aria-proptypes: warn jsx-a11y/aria-role: warn jsx-a11y/aria-unsupported-elements: warn jsx-a11y/heading-has-content: warn - jsx-a11y/href-no-hash: warn jsx-a11y/html-has-lang: warn jsx-a11y/iframe-has-title: warn jsx-a11y/img-redundant-alt: warn diff --git a/.rubocop.yml b/.rubocop.yml index a36aa5cae..6faeaca6f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -107,5 +107,8 @@ Style/RegexpLiteral: Style/SymbolArray: Enabled: false -Style/TrailingCommaInLiteral: +Style/TrailingCommaInArrayLiteral: + EnforcedStyleForMultiline: 'comma' + +Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: 'comma' diff --git a/app/controllers/api/v1/domain_blocks_controller.rb b/app/controllers/api/v1/domain_blocks_controller.rb index ae6ad7936..e55d622c3 100644 --- a/app/controllers/api/v1/domain_blocks_controller.rb +++ b/app/controllers/api/v1/domain_blocks_controller.rb @@ -15,7 +15,8 @@ class Api::V1::DomainBlocksController < Api::BaseController end def create - BlockDomainFromAccountService.new.call(current_account, domain_block_params[:domain]) + current_account.block_domain!(domain_block_params[:domain]) + AfterAccountDomainBlockWorker.perform_async(current_account.id, domain_block_params[:domain]) render_empty end diff --git a/app/controllers/api/v1/timelines/direct_controller.rb b/app/controllers/api/v1/timelines/direct_controller.rb index d455227eb..ef64078be 100644 --- a/app/controllers/api/v1/timelines/direct_controller.rb +++ b/app/controllers/api/v1/timelines/direct_controller.rb @@ -23,15 +23,18 @@ class Api::V1::Timelines::DirectController < Api::BaseController end def direct_statuses - direct_timeline_statuses.paginate_by_max_id( - limit_param(DEFAULT_STATUSES_LIMIT), - params[:max_id], - params[:since_id] - ) + direct_timeline_statuses end def direct_timeline_statuses - Status.as_direct_timeline(current_account) + # this query requires built in pagination. + Status.as_direct_timeline( + current_account, + limit_param(DEFAULT_STATUSES_LIMIT), + params[:max_id], + params[:since_id], + true # returns array of cache_ids object + ) end def insert_pagination_headers diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb new file mode 100644 index 000000000..2e91d68ee --- /dev/null +++ b/app/controllers/api/v2/search_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class Api::V2::SearchController < Api::V1::SearchController + def index + @search = Search.new(search) + render json: @search, serializer: REST::V2::SearchSerializer + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5b22f17c6..29ba6cad6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -20,6 +20,7 @@ class ApplicationController < ActionController::Base rescue_from ActionController::RoutingError, with: :not_found rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity + rescue_from ActionController::UnknownFormat, with: :not_acceptable rescue_from Mastodon::NotPermittedError, with: :forbidden before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? @@ -73,6 +74,10 @@ class ApplicationController < ActionController::Base respond_with_error(422) end + def not_acceptable + respond_with_error(406) + end + def single_user_mode? @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists? end diff --git a/app/controllers/intents_controller.rb b/app/controllers/intents_controller.rb index 504befd1f..56129d69a 100644 --- a/app/controllers/intents_controller.rb +++ b/app/controllers/intents_controller.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true class IntentsController < ApplicationController - def show - uri = Addressable::URI.parse(params[:uri]) + before_action :check_uri + rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri + def show if uri.scheme == 'web+mastodon' case uri.host when 'follow' @@ -15,4 +16,18 @@ class IntentsController < ApplicationController not_found end + + private + + def check_uri + not_found if uri.blank? + end + + def handle_invalid_uri + not_found + end + + def uri + @uri ||= Addressable::URI.parse(params[:uri]) + end end diff --git a/app/controllers/settings/follower_domains_controller.rb b/app/controllers/settings/follower_domains_controller.rb index 91b521e7f..a128bd136 100644 --- a/app/controllers/settings/follower_domains_controller.rb +++ b/app/controllers/settings/follower_domains_controller.rb @@ -13,7 +13,7 @@ class Settings::FollowerDomainsController < ApplicationController def update domains = bulk_params[:select] || [] - SoftBlockDomainFollowersWorker.push_bulk(domains) do |domain| + AfterAccountDomainBlockWorker.push_bulk(domains) do |domain| [current_account.id, domain] end diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js index 882c1709e..b670d25c3 100644 --- a/app/javascript/mastodon/actions/search.js +++ b/app/javascript/mastodon/actions/search.js @@ -33,7 +33,7 @@ export function submitSearch() { dispatch(fetchSearchRequest()); - api(getState).get('/api/v1/search', { + api(getState).get('/api/v2/search', { params: { q: value, resolve: true, diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 849cb4f5a..3e1e5f270 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -29,6 +29,8 @@ export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; export const STATUS_REVEAL = 'STATUS_REVEAL'; export const STATUS_HIDE = 'STATUS_HIDE'; +export const REDRAFT = 'REDRAFT'; + export function fetchStatusRequest(id, skipLoading) { return { type: STATUS_FETCH_REQUEST, @@ -131,14 +133,27 @@ export function fetchStatusFail(id, error, skipLoading) { }; }; -export function deleteStatus(id) { +export function redraft(status) { + return { + type: REDRAFT, + status, + }; +}; + +export function deleteStatus(id, withRedraft = false) { return (dispatch, getState) => { + const status = getState().getIn(['statuses', id]); + dispatch(deleteStatusRequest(id)); api(getState).delete(`/api/v1/statuses/${id}`).then(() => { evictStatus(id); dispatch(deleteStatusSuccess(id)); dispatch(deleteFromTimelines(id)); + + if (withRedraft) { + dispatch(redraft(status)); + } }).catch(error => { dispatch(deleteStatusFail(id, error)); }); diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 8f54dfd8a..11a199db6 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -13,21 +13,9 @@ export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; -export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE'; - export function updateTimeline(timeline, status) { return (dispatch, getState) => { const references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : []; - const parents = []; - - if (status.in_reply_to_id) { - let parent = getState().getIn(['statuses', status.in_reply_to_id]); - - while (parent && parent.get('in_reply_to_id')) { - parents.push(parent.get('id')); - parent = getState().getIn(['statuses', parent.get('in_reply_to_id')]); - } - } dispatch(importFetchedStatus(status)); @@ -37,14 +25,6 @@ export function updateTimeline(timeline, status) { status, references, }); - - if (parents.length > 0) { - dispatch({ - type: TIMELINE_CONTEXT_UPDATE, - status, - references: parents, - }); - } }; }; diff --git a/app/javascript/mastodon/components/collapsable.js b/app/javascript/mastodon/components/collapsable.js deleted file mode 100644 index d5d431186..000000000 --- a/app/javascript/mastodon/components/collapsable.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import Motion from '../features/ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import PropTypes from 'prop-types'; - -const Collapsable = ({ fullHeight, isVisible, children }) => ( - - {({ opacity, height }) => ( -
- {children} -
- )} -
-); - -Collapsable.propTypes = { - fullHeight: PropTypes.number.isRequired, - isVisible: PropTypes.bool.isRequired, - children: PropTypes.node.isRequired, -}; - -export default Collapsable; diff --git a/app/javascript/mastodon/components/hashtag.js b/app/javascript/mastodon/components/hashtag.js new file mode 100644 index 000000000..a407df31e --- /dev/null +++ b/app/javascript/mastodon/components/hashtag.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { Sparklines, SparklinesCurve } from 'react-sparklines'; +import { Link } from 'react-router-dom'; +import { FormattedMessage } from 'react-intl'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { shortNumberFormat } from '../utils/numbers'; + +const Hashtag = ({ hashtag }) => ( +
+
+ + #{hashtag.get('name')} + + + {shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))} }} /> +
+ +
+ {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))} +
+ +
+ day.get('uses')).toArray()}> + + +
+
+); + +Hashtag.propTypes = { + hashtag: ImmutablePropTypes.map.isRequired, +}; + +export default Hashtag; diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index fd6858d05..4b433f32c 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -25,6 +25,7 @@ export default class ScrollableList extends PureComponent { isLoading: PropTypes.bool, hasMore: PropTypes.bool, prepend: PropTypes.node, + alwaysPrepend: PropTypes.bool, emptyMessage: PropTypes.node, children: PropTypes.node, }; @@ -140,7 +141,7 @@ export default class ScrollableList extends PureComponent { } render () { - const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage, onLoadMore } = this.props; + const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; const childrenCount = React.Children.count(children); @@ -172,8 +173,12 @@ export default class ScrollableList extends PureComponent { ); } else { scrollableArea = ( -
- {emptyMessage} +
+ {alwaysPrepend && prepend} + +
+ {emptyMessage} +
); } diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index d605dbc8a..0ae21e3f0 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -9,6 +9,7 @@ import { me } from '../initial_state'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, + redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, @@ -88,6 +89,10 @@ export default class StatusActionBar extends ImmutablePureComponent { this.props.onDelete(this.props.status); } + handleRedraftClick = () => { + this.props.onDelete(this.props.status, true); + } + handlePinClick = () => { this.props.onPin(this.props.status); } @@ -159,6 +164,7 @@ export default class StatusActionBar extends ImmutablePureComponent { } menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); + menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); } else { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js index 0c971ceb0..1c34d0640 100644 --- a/app/javascript/mastodon/components/status_list.js +++ b/app/javascript/mastodon/components/status_list.js @@ -24,6 +24,7 @@ export default class StatusList extends ImmutablePureComponent { hasMore: PropTypes.bool, prepend: PropTypes.node, emptyMessage: PropTypes.node, + alwaysPrepend: PropTypes.bool, }; static defaultProps = { diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index f22509edf..3e7b5215b 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -33,6 +33,8 @@ import { showAlertForError } from '../actions/alerts'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, + redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, + redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' }, blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, }); @@ -91,14 +93,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ })); }, - onDelete (status) { + onDelete (status, withRedraft = false) { if (!deleteModal) { - dispatch(deleteStatus(status.get('id'))); + dispatch(deleteStatus(status.get('id'), withRedraft)); } else { dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.deleteMessage), - confirm: intl.formatMessage(messages.deleteConfirm), - onConfirm: () => dispatch(deleteStatus(status.get('id'))), + message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), + confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), + onConfirm: () => dispatch(deleteStatus(status.get('id'), withRedraft)), })); } }, diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js index 23dbf32bc..2d0f72be2 100644 --- a/app/javascript/mastodon/features/account/components/action_bar.js +++ b/app/javascript/mastodon/features/account/components/action_bar.js @@ -3,8 +3,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import { Link } from 'react-router-dom'; -import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { me } from '../../../initial_state'; +import { shortNumberFormat } from '../../../utils/numbers'; const messages = defineMessages({ mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, @@ -23,6 +24,14 @@ const messages = defineMessages({ unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' }, showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, + pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, + preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, + follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, + favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, + lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, + blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, + domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, + mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, }); @injectIntl @@ -54,17 +63,29 @@ export default class ActionBar extends React.PureComponent { let menu = []; let extraInfo = ''; - menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); - menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect }); + if (account.get('id') !== me) { + menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); + menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect }); + menu.push(null); + } if ('share' in navigator) { menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); + menu.push(null); } - menu.push(null); - if (account.get('id') === me) { menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); + menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' }); + menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); + menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' }); + menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' }); + menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); + menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' }); } else { if (account.getIn(['relationship', 'following'])) { if (account.getIn(['relationship', 'showing_reblogs'])) { @@ -126,17 +147,17 @@ export default class ActionBar extends React.PureComponent {
- + {shortNumberFormat(account.get('statuses_count'))} - + {shortNumberFormat(account.get('following_count'))} - + {shortNumberFormat(account.get('followers_count'))}
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 7358053da..dd2cd406b 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -14,6 +14,7 @@ const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, + edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, }); class Avatar extends ImmutablePureComponent { @@ -74,6 +75,10 @@ export default class Header extends ImmutablePureComponent { intl: PropTypes.object.isRequired, }; + openEditProfile = () => { + window.open('/settings/profile', '_blank'); + } + render () { const { account, intl } = this.props; @@ -118,6 +123,12 @@ export default class Header extends ImmutablePureComponent { ); } + } else { + actionBtn = ( +
+ +
+ ); } if (account.get('moved') && !account.getIn(['relationship', 'following'])) { diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js index 4d5308219..7681430b7 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js @@ -96,7 +96,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onBlockDomain (domain) { dispatch(openModal('CONFIRM', { - message: {domain} }} />, + message: {domain} }} />, confirm: intl.formatMessage(messages.blockDomainConfirm), onConfirm: () => dispatch(blockDomain(domain)), })); diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js index 58b8a8b73..d375edbd5 100644 --- a/app/javascript/mastodon/features/community_timeline/index.js +++ b/app/javascript/mastodon/features/community_timeline/index.js @@ -8,7 +8,7 @@ import ColumnHeader from '../../components/column_header'; import { expandCommunityTimeline } from '../../actions/timelines'; import { addColumn, removeColumn, moveColumn, changeColumnParams } from '../../actions/columns'; import ColumnSettingsContainer from './containers/column_settings_container'; -// import SectionHeadline from './components/section_headline'; +import SectionHeadline from './components/section_headline'; import { connectCommunityStream } from '../../actions/streaming'; const messages = defineMessages({ @@ -100,17 +100,15 @@ export default class CommunityTimeline extends React.PureComponent { const { intl, hasUnread, columnId, multiColumn, onlyMedia } = this.props; const pinned = !!columnId; - // pending - // - // const headline = ( - // - // ); + const headline = ( + + ); return ( @@ -128,7 +126,8 @@ export default class CommunityTimeline extends React.PureComponent { { - this._restoreCaret = null; this.props.onSuggestionSelected(tokenStart, token, value); } @@ -116,9 +115,9 @@ export default class ComposeForm extends ImmutablePureComponent { if (this.props.preselectDate !== prevProps.preselectDate) { selectionEnd = this.props.text.length; selectionStart = this.props.text.search(/\s/) + 1; - } else if (typeof this._restoreCaret === 'number') { - selectionStart = this._restoreCaret; - selectionEnd = this._restoreCaret; + } else if (typeof this.props.caretPosition === 'number') { + selectionStart = this.props.caretPosition; + selectionEnd = this.props.caretPosition; } else { selectionEnd = this.props.text.length; selectionStart = selectionEnd; @@ -129,19 +128,29 @@ export default class ComposeForm extends ImmutablePureComponent { } else if(prevProps.is_submitting && !this.props.is_submitting) { this.autosuggestTextarea.textarea.focus(); } + + if (this.props.spoiler !== prevProps.spoiler) { + if (this.props.spoiler) { + this.spoilerText.focus(); + } else { + this.autosuggestTextarea.textarea.focus(); + } + } } setAutosuggestTextarea = (c) => { this.autosuggestTextarea = c; } + setSpoilerText = (c) => { + this.spoilerText = c; + } + handleEmojiPick = (data) => { const { text } = this.props; const position = this.autosuggestTextarea.textarea.selectionStart; - const emojiChar = data.native; const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]); - this._restoreCaret = position + emojiChar.length + 1 + (needsSpace ? 1 : 0); this.props.onPickEmoji(position, data, needsSpace); } @@ -162,17 +171,15 @@ export default class ComposeForm extends ImmutablePureComponent {
- -
- -
-
- +
+ +
+
`${assetHost}/emoji/sheet.png`; +const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`; const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; const categoriesSort = [ diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js index 84455563c..c351b84bb 100644 --- a/app/javascript/mastodon/features/compose/components/search_results.js +++ b/app/javascript/mastodon/features/compose/components/search_results.js @@ -3,8 +3,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage } from 'react-intl'; import AccountContainer from '../../../containers/account_container'; import StatusContainer from '../../../containers/status_container'; -import { Link } from 'react-router-dom'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import Hashtag from '../../../components/hashtag'; export default class SearchResults extends ImmutablePureComponent { @@ -22,7 +22,7 @@ export default class SearchResults extends ImmutablePureComponent { count += results.get('accounts').size; accounts = (
-
+
{results.get('accounts').map(accountId => )}
@@ -33,7 +33,7 @@ export default class SearchResults extends ImmutablePureComponent { count += results.get('statuses').size; statuses = (
-
+
{results.get('statuses').map(statusId => )}
@@ -44,13 +44,9 @@ export default class SearchResults extends ImmutablePureComponent { count += results.get('hashtags').size; hashtags = (
-
+
- {results.get('hashtags').map(hashtag => ( - - #{hashtag} - - ))} + {results.get('hashtags').map(hashtag => )}
); } @@ -58,6 +54,7 @@ export default class SearchResults extends ImmutablePureComponent { return (
+
diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js index c3aa580ee..3822dd711 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -19,6 +19,7 @@ const mapStateToProps = state => ({ spoiler_text: state.getIn(['compose', 'spoiler_text']), privacy: state.getIn(['compose', 'privacy']), focusDate: state.getIn(['compose', 'focusDate']), + caretPosition: state.getIn(['compose', 'caretPosition']), preselectDate: state.getIn(['compose', 'preselectDate']), is_submitting: state.getIn(['compose', 'is_submitting']), is_uploading: state.getIn(['compose', 'is_uploading']), diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index 19aae0332..df1ec4915 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -75,7 +75,7 @@ export default class Compose extends React.PureComponent { const { columns } = this.props; header = (