diff --git a/.codeclimate.yml b/.codeclimate.yml index 21e6b33..58f6b3d 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 24b6b01..3047f75 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 576e5b7..fbda265 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 a36aa5c..6faeaca 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 ae6ad79..e55d622 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 d455227..ef64078 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 0000000..2e91d68 --- /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 5b22f17..29ba6ca 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 504befd..56129d6 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 91b521e..a128bd1 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/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index c015d3a..10a39e0 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -50,13 +50,14 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); normalStatus.hidden = normalOldStatus.get('hidden'); } else { - const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); - const emojiMap = makeEmojiMap(normalStatus); + const spoilerText = normalStatus.spoiler_text || ''; + const searchContent = [spoilerText, status.content].join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); + const emojiMap = makeEmojiMap(normalStatus); normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); - normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''), emojiMap); - normalStatus.hidden = normalStatus.sensitive; + normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); + normalStatus.hidden = spoilerText.length > 0 || normalStatus.sensitive; } return normalStatus; diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js index 882c170..b670d25 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 849cb4f..3e1e5f2 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 8f54dfd..11a199d 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 d5d4311..0000000 --- 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 0000000..a407df3 --- /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 fd6858d..4b433f3 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 d605dbc..0ae21e3 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 0c971ce..1c34d06 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 f22509e..3e7b521 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 23dbf32..2d0f72b 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 7358053..dd2cd40 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 4d53082..7681430 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 58b8a8b..d375edb 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 { +
+ +
+ + ); + } + +} diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index 9cd39be..83f2f4d 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -7,7 +7,6 @@ import ReplyIndicatorContainer from '../containers/reply_indicator_container'; import AutosuggestTextarea from '../../../components/autosuggest_textarea'; import UploadButtonContainer from '../containers/upload_button_container'; import { defineMessages, injectIntl } from 'react-intl'; -import Collapsable from '../../../components/collapsable'; import SpoilerButtonContainer from '../containers/spoiler_button_container'; import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; import SensitiveButtonContainer from '../containers/sensitive_button_container'; @@ -40,6 +39,7 @@ export default class ComposeForm extends ImmutablePureComponent { privacy: PropTypes.string, spoiler_text: PropTypes.string, focusDate: PropTypes.instanceOf(Date), + caretPosition: PropTypes.number, preselectDate: PropTypes.instanceOf(Date), is_submitting: PropTypes.bool, is_uploading: PropTypes.bool, @@ -96,7 +96,6 @@ export default class ComposeForm extends ImmutablePureComponent { } onSuggestionSelected = (tokenStart, token, value) => { - 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/navigation_bar.js b/app/javascript/mastodon/features/compose/components/navigation_bar.js index 3014c40..9910eb4 100644 --- a/app/javascript/mastodon/features/compose/components/navigation_bar.js +++ b/app/javascript/mastodon/features/compose/components/navigation_bar.js @@ -1,9 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import ActionBar from './action_bar'; import Avatar from '../../../components/avatar'; -import IconButton from '../../../components/icon_button'; import Permalink from '../../../components/permalink'; +import IconButton from '../../../components/icon_button'; import { FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -30,7 +31,10 @@ export default class NavigationBar extends ImmutablePureComponent {
- +
+ + +
); } diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js index 8445556..c351b84 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 c3aa580..3822dd7 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 19aae03..df1ec49 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 = (