diff options
Diffstat (limited to 'app')
47 files changed, 229 insertions, 139 deletions
diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb index 53f7f1e27..50bace217 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -9,17 +9,8 @@ module WellKnown def show @account = Account.find_local!(username_from_resource) - respond_to do |format| - format.any(:json, :html) do - render json: @account, serializer: WebfingerSerializer, content_type: 'application/jrd+json' - end - - format.xml do - render content_type: 'application/xrd+xml' - end - end - expires_in 3.days, public: true + render json: @account, serializer: WebfingerSerializer, content_type: 'application/jrd+json' rescue ActiveRecord::RecordNotFound head 404 end diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.js index 8d982208f..764dcea69 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.js @@ -182,6 +182,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent { className='value' disabled={disabled} icon={icon} + inverted onClick={handleToggle} size={18} style={{ diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js index ed52b1997..92348b000 100644 --- a/app/javascript/flavours/glitch/features/compose/components/options.js +++ b/app/javascript/flavours/glitch/features/compose/components/options.js @@ -7,7 +7,7 @@ import spring from 'react-motion/lib/spring'; // Components. import IconButton from 'flavours/glitch/components/icon_button'; -import TextIconButton from 'flavours/glitch/components/text_icon_button'; +import TextIconButton from './text_icon_button'; import Dropdown from './dropdown'; import ImmutablePureComponent from 'react-immutable-pure-component'; diff --git a/app/javascript/flavours/glitch/components/text_icon_button.js b/app/javascript/flavours/glitch/features/compose/components/text_icon_button.js index 9c8ffab1f..7f2005060 100644 --- a/app/javascript/flavours/glitch/components/text_icon_button.js +++ b/app/javascript/flavours/glitch/features/compose/components/text_icon_button.js @@ -1,6 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; +const iconStyle = { + height: null, + lineHeight: '27px', + width: `${18 * 1.28571429}px`, +}; + export default class TextIconButton extends React.PureComponent { static propTypes = { @@ -20,7 +26,15 @@ export default class TextIconButton extends React.PureComponent { const { label, title, active, ariaControls } = this.props; return ( - <button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={this.handleClick} aria-controls={ariaControls}> + <button + title={title} + aria-label={title} + className={`text-icon-button ${active ? 'active' : ''}`} + aria-expanded={active} + onClick={this.handleClick} + aria-controls={ariaControls} + style={iconStyle} + > {label} </button> ); diff --git a/app/javascript/flavours/glitch/features/list_editor/components/edit_list_form.js b/app/javascript/flavours/glitch/features/list_editor/components/edit_list_form.js index 24aaf82ac..bf5a8de35 100644 --- a/app/javascript/flavours/glitch/features/list_editor/components/edit_list_form.js +++ b/app/javascript/flavours/glitch/features/list_editor/components/edit_list_form.js @@ -11,7 +11,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ value: state.getIn(['listEditor', 'title']), - disabled: !state.getIn(['listEditor', 'isChanged']), + disabled: !state.getIn(['listEditor', 'isChanged']) || !state.getIn(['listEditor', 'title']), }); const mapDispatchToProps = dispatch => ({ diff --git a/app/javascript/flavours/glitch/features/lists/components/new_list_form.js b/app/javascript/flavours/glitch/features/lists/components/new_list_form.js index 61fcbeaf9..eb5b6188a 100644 --- a/app/javascript/flavours/glitch/features/lists/components/new_list_form.js +++ b/app/javascript/flavours/glitch/features/lists/components/new_list_form.js @@ -66,7 +66,7 @@ export default class NewListForm extends React.PureComponent { </label> <IconButton - disabled={disabled} + disabled={disabled || !value} icon='plus' title={title} onClick={this.handleClick} diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 9f96a3154..6942170f2 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -118,20 +118,29 @@ display: inline-block; padding: 0; color: $action-button-color; - border: none; + border: 0; + border-radius: 4px; background: transparent; cursor: pointer; - transition: color 100ms ease-in; + transition: all 100ms ease-in; + transition-property: background-color, color; &:hover, &:active, &:focus { color: lighten($action-button-color, 7%); - transition: color 200ms ease-out; + background-color: rgba($action-button-color, 0.15); + transition: all 200ms ease-out; + transition-property: background-color, color; + } + + &:focus { + background-color: rgba($action-button-color, 0.3); } &.disabled { color: darken($action-button-color, 13%); + background-color: transparent; cursor: default; } @@ -156,10 +165,16 @@ &:active, &:focus { color: darken($lighter-text-color, 7%); + background-color: rgba($lighter-text-color, 0.15); + } + + &:focus { + background-color: rgba($lighter-text-color, 0.3); } &.disabled { color: lighten($lighter-text-color, 7%); + background-color: transparent; } &.active { @@ -186,7 +201,8 @@ .text-icon-button { color: $lighter-text-color; - border: none; + border: 0; + border-radius: 4px; background: transparent; cursor: pointer; font-weight: 600; @@ -194,17 +210,25 @@ padding: 0 3px; line-height: 27px; outline: 0; - transition: color 100ms ease-in; + transition: all 100ms ease-in; + transition-property: background-color, color; &:hover, &:active, &:focus { color: darken($lighter-text-color, 7%); - transition: color 200ms ease-out; + background-color: rgba($lighter-text-color, 0.15); + transition: all 200ms ease-out; + transition-property: background-color, color; + } + + &:focus { + background-color: rgba($lighter-text-color, 0.3); } &.disabled { color: lighten($lighter-text-color, 20%); + background-color: transparent; cursor: default; } diff --git a/app/javascript/mastodon/components/column.js b/app/javascript/mastodon/components/column.js index d45387463..55e3bfd5e 100644 --- a/app/javascript/mastodon/components/column.js +++ b/app/javascript/mastodon/components/column.js @@ -8,10 +8,11 @@ export default class Column extends React.PureComponent { static propTypes = { children: PropTypes.node, label: PropTypes.string, + bindToDocument: PropTypes.bool, }; scrollTop () { - const scrollable = this.node.querySelector('.scrollable'); + const scrollable = this.props.bindToDocument ? document.scrollingElement : this.node.querySelector('.scrollable'); if (!scrollable) { return; @@ -33,11 +34,19 @@ export default class Column extends React.PureComponent { } componentDidMount () { - this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + if (this.props.bindToDocument) { + document.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + } else { + this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false); + } } componentWillUnmount () { - this.node.removeEventListener('wheel', this.handleWheel); + if (this.props.bindToDocument) { + document.removeEventListener('wheel', this.handleWheel); + } else { + this.node.removeEventListener('wheel', this.handleWheel); + } } render () { diff --git a/app/javascript/mastodon/components/column_back_button.js b/app/javascript/mastodon/components/column_back_button.js index f41045787..cc0e5c07c 100644 --- a/app/javascript/mastodon/components/column_back_button.js +++ b/app/javascript/mastodon/components/column_back_button.js @@ -2,6 +2,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; import Icon from 'mastodon/components/icon'; +import { createPortal } from 'react-dom'; export default class ColumnBackButton extends React.PureComponent { @@ -9,6 +10,10 @@ export default class ColumnBackButton extends React.PureComponent { router: PropTypes.object, }; + static propTypes = { + multiColumn: PropTypes.bool, + }; + handleClick = () => { if (window.history && window.history.length === 1) { this.context.router.history.push('/'); @@ -18,12 +23,20 @@ export default class ColumnBackButton extends React.PureComponent { } render () { - return ( + const { multiColumn } = this.props; + + const component = ( <button onClick={this.handleClick} className='column-back-button'> <Icon id='chevron-left' className='column-back-button__icon' fixedWidth /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' /> </button> ); + + if (multiColumn) { + return component; + } else { + return createPortal(component, document.getElementById('tabs-bar__portal')); + } } } diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js index f33c689e7..89c5fe723 100644 --- a/app/javascript/mastodon/components/column_header.js +++ b/app/javascript/mastodon/components/column_header.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { createPortal } from 'react-dom'; import classNames from 'classnames'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import Icon from 'mastodon/components/icon'; @@ -28,6 +29,7 @@ class ColumnHeader extends React.PureComponent { showBackButton: PropTypes.bool, children: PropTypes.node, pinned: PropTypes.bool, + placeholder: PropTypes.bool, onPin: PropTypes.func, onMove: PropTypes.func, onClick: PropTypes.func, @@ -79,7 +81,7 @@ class ColumnHeader extends React.PureComponent { } render () { - const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage } } = this.props; + const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder } = this.props; const { collapsed, animating } = this.state; const wrapperClassName = classNames('column-header__wrapper', { @@ -146,7 +148,7 @@ class ColumnHeader extends React.PureComponent { const hasTitle = icon && title; - return ( + const component = ( <div className={wrapperClassName}> <h1 className={buttonClassName}> {hasTitle && ( @@ -172,6 +174,12 @@ class ColumnHeader extends React.PureComponent { </div> </div> ); + + if (multiColumn || placeholder) { + return component; + } else { + return createPortal(component, document.getElementById('tabs-bar__portal')); + } } } diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js index 5d6a53e18..f1a665d8f 100644 --- a/app/javascript/mastodon/features/account_gallery/index.js +++ b/app/javascript/mastodon/features/account_gallery/index.js @@ -56,6 +56,7 @@ class AccountGallery extends ImmutablePureComponent { isLoading: PropTypes.bool, hasMore: PropTypes.bool, isAccount: PropTypes.bool, + multiColumn: PropTypes.bool, }; state = { @@ -116,7 +117,7 @@ class AccountGallery extends ImmutablePureComponent { } render () { - const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount } = this.props; + const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn } = this.props; const { width } = this.state; if (!isAccount) { @@ -143,7 +144,7 @@ class AccountGallery extends ImmutablePureComponent { return ( <Column> - <ColumnBackButton /> + <ColumnBackButton multiColumn={multiColumn} /> <ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={shouldUpdateScroll}> <div className='scrollable scrollable--flex' onScroll={this.handleScroll}> diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js index 9914b7e65..69bab1e86 100644 --- a/app/javascript/mastodon/features/account_timeline/index.js +++ b/app/javascript/mastodon/features/account_timeline/index.js @@ -100,7 +100,7 @@ class AccountTimeline extends ImmutablePureComponent { return ( <Column> - <ColumnBackButton /> + <ColumnBackButton multiColumn={multiColumn} /> <StatusList prepend={<HeaderContainer accountId={this.props.params.accountId} />} diff --git a/app/javascript/mastodon/features/blocks/index.js b/app/javascript/mastodon/features/blocks/index.js index 8fb0f051b..051431ed2 100644 --- a/app/javascript/mastodon/features/blocks/index.js +++ b/app/javascript/mastodon/features/blocks/index.js @@ -57,7 +57,7 @@ class Blocks extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.blocks' defaultMessage="You haven't blocked any users yet." />; return ( - <Column icon='ban' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='ban' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <ScrollableList scrollKey='blocks' diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js index 2f6999f61..f95fa4970 100644 --- a/app/javascript/mastodon/features/community_timeline/index.js +++ b/app/javascript/mastodon/features/community_timeline/index.js @@ -105,7 +105,7 @@ class CommunityTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef} label={intl.formatMessage(messages.title)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> <ColumnHeader icon='users' active={hasUnread} diff --git a/app/javascript/mastodon/features/compose/components/text_icon_button.js b/app/javascript/mastodon/features/compose/components/text_icon_button.js index 9c8ffab1f..f0b133538 100644 --- a/app/javascript/mastodon/features/compose/components/text_icon_button.js +++ b/app/javascript/mastodon/features/compose/components/text_icon_button.js @@ -1,6 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; +const iconStyle = { + height: null, + lineHeight: '27px', + width: `${18 * 1.28571429}px`, +}; + export default class TextIconButton extends React.PureComponent { static propTypes = { @@ -20,7 +26,14 @@ export default class TextIconButton extends React.PureComponent { const { label, title, active, ariaControls } = this.props; return ( - <button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={this.handleClick} aria-controls={ariaControls}> + <button + title={title} + aria-label={title} + className={`text-icon-button ${active ? 'active' : ''}`} + aria-expanded={active} + onClick={this.handleClick} + aria-controls={ariaControls} style={iconStyle} + > {label} </button> ); diff --git a/app/javascript/mastodon/features/direct_timeline/index.js b/app/javascript/mastodon/features/direct_timeline/index.js index d202f3bfd..5ce795760 100644 --- a/app/javascript/mastodon/features/direct_timeline/index.js +++ b/app/javascript/mastodon/features/direct_timeline/index.js @@ -75,7 +75,7 @@ class DirectTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef} label={intl.formatMessage(messages.title)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> <ColumnHeader icon='envelope' active={hasUnread} diff --git a/app/javascript/mastodon/features/domain_blocks/index.js b/app/javascript/mastodon/features/domain_blocks/index.js index 16e200b31..482245c86 100644 --- a/app/javascript/mastodon/features/domain_blocks/index.js +++ b/app/javascript/mastodon/features/domain_blocks/index.js @@ -58,7 +58,7 @@ class Blocks extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no hidden domains yet.' />; return ( - <Column icon='minus-circle' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='minus-circle' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <ScrollableList scrollKey='domain_blocks' diff --git a/app/javascript/mastodon/features/favourited_statuses/index.js b/app/javascript/mastodon/features/favourited_statuses/index.js index 8c7b23869..db8a3f815 100644 --- a/app/javascript/mastodon/features/favourited_statuses/index.js +++ b/app/javascript/mastodon/features/favourited_statuses/index.js @@ -74,7 +74,7 @@ class Favourites extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favourite toots yet. When you favourite one, it will show up here." />; return ( - <Column ref={this.setRef} label={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}> <ColumnHeader icon='star' title={intl.formatMessage(messages.heading)} diff --git a/app/javascript/mastodon/features/favourites/index.js b/app/javascript/mastodon/features/favourites/index.js index 464f7aeb0..62d3c2f06 100644 --- a/app/javascript/mastodon/features/favourites/index.js +++ b/app/javascript/mastodon/features/favourites/index.js @@ -51,7 +51,7 @@ class Favourites extends ImmutablePureComponent { return ( <Column> - <ColumnBackButton /> + <ColumnBackButton multiColumn={multiColumn} /> <ScrollableList scrollKey='favourites' diff --git a/app/javascript/mastodon/features/follow_requests/index.js b/app/javascript/mastodon/features/follow_requests/index.js index 570cf57c8..57ef44145 100644 --- a/app/javascript/mastodon/features/follow_requests/index.js +++ b/app/javascript/mastodon/features/follow_requests/index.js @@ -57,7 +57,7 @@ class FollowRequests extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />; return ( - <Column icon='user-plus' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='user-plus' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <ScrollableList scrollKey='follow_requests' diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js index dce05bdc6..3913bf8d0 100644 --- a/app/javascript/mastodon/features/followers/index.js +++ b/app/javascript/mastodon/features/followers/index.js @@ -78,7 +78,7 @@ class Followers extends ImmutablePureComponent { return ( <Column> - <ColumnBackButton /> + <ColumnBackButton multiColumn={multiColumn} /> <ScrollableList scrollKey='followers' diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js index d9f2ef079..8e126f4c3 100644 --- a/app/javascript/mastodon/features/following/index.js +++ b/app/javascript/mastodon/features/following/index.js @@ -78,7 +78,7 @@ class Following extends ImmutablePureComponent { return ( <Column> - <ColumnBackButton /> + <ColumnBackButton multiColumn={multiColumn} /> <ScrollableList scrollKey='following' diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index fc7840ec1..791f22d47 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -148,7 +148,7 @@ class GettingStarted extends ImmutablePureComponent { } return ( - <Column label={intl.formatMessage(messages.menu)}> + <Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.menu)}> {multiColumn && <div className='column-header__wrapper'> <h1 className='column-header'> <button> diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js index c50f6a79a..28200e6c2 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/index.js +++ b/app/javascript/mastodon/features/hashtag_timeline/index.js @@ -135,7 +135,7 @@ class HashtagTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef} label={`#${id}`}> + <Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}> <ColumnHeader icon='hashtag' active={hasUnread} diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js index bf8ff117b..1cafb88ed 100644 --- a/app/javascript/mastodon/features/home_timeline/index.js +++ b/app/javascript/mastodon/features/home_timeline/index.js @@ -98,7 +98,7 @@ class HomeTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef} label={intl.formatMessage(messages.title)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> <ColumnHeader icon='home' active={hasUnread} diff --git a/app/javascript/mastodon/features/keyboard_shortcuts/index.js b/app/javascript/mastodon/features/keyboard_shortcuts/index.js index 01b45652c..90dc87cbb 100644 --- a/app/javascript/mastodon/features/keyboard_shortcuts/index.js +++ b/app/javascript/mastodon/features/keyboard_shortcuts/index.js @@ -18,10 +18,10 @@ class KeyboardShortcuts extends ImmutablePureComponent { }; render () { - const { intl } = this.props; + const { intl, multiColumn } = this.props; return ( - <Column icon='question' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='question' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <div className='keyboard-shortcuts scrollable optionally-scrollable'> <table> diff --git a/app/javascript/mastodon/features/list_editor/components/edit_list_form.js b/app/javascript/mastodon/features/list_editor/components/edit_list_form.js index 3dc59c12e..3ccab12a8 100644 --- a/app/javascript/mastodon/features/list_editor/components/edit_list_form.js +++ b/app/javascript/mastodon/features/list_editor/components/edit_list_form.js @@ -11,7 +11,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ value: state.getIn(['listEditor', 'title']), - disabled: !state.getIn(['listEditor', 'isChanged']), + disabled: !state.getIn(['listEditor', 'isChanged']) || !state.getIn(['listEditor', 'title']), }); const mapDispatchToProps = dispatch => ({ diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js index 844c93db1..f3205b2bf 100644 --- a/app/javascript/mastodon/features/list_timeline/index.js +++ b/app/javascript/mastodon/features/list_timeline/index.js @@ -148,14 +148,14 @@ class ListTimeline extends React.PureComponent { } else if (list === false) { return ( <Column> - <ColumnBackButton /> + <ColumnBackButton multiColumn={multiColumn} /> <MissingIndicator /> </Column> ); } return ( - <Column ref={this.setRef} label={title}> + <Column bindToDocument={!multiColumn} ref={this.setRef} label={title}> <ColumnHeader icon='list-ul' active={hasUnread} diff --git a/app/javascript/mastodon/features/lists/components/new_list_form.js b/app/javascript/mastodon/features/lists/components/new_list_form.js index 739246640..7faf50be8 100644 --- a/app/javascript/mastodon/features/lists/components/new_list_form.js +++ b/app/javascript/mastodon/features/lists/components/new_list_form.js @@ -66,7 +66,7 @@ class NewListForm extends React.PureComponent { </label> <IconButton - disabled={disabled} + disabled={disabled || !value} icon='plus' title={title} onClick={this.handleClick} diff --git a/app/javascript/mastodon/features/lists/index.js b/app/javascript/mastodon/features/lists/index.js index a06e0b934..7f7f5009c 100644 --- a/app/javascript/mastodon/features/lists/index.js +++ b/app/javascript/mastodon/features/lists/index.js @@ -61,7 +61,7 @@ class Lists extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.lists' defaultMessage="You don't have any lists yet. When you create one, it will show up here." />; return ( - <Column icon='list-ul' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='list-ul' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <NewListForm /> diff --git a/app/javascript/mastodon/features/mutes/index.js b/app/javascript/mastodon/features/mutes/index.js index 57d8b9915..91dd268c1 100644 --- a/app/javascript/mastodon/features/mutes/index.js +++ b/app/javascript/mastodon/features/mutes/index.js @@ -57,7 +57,7 @@ class Mutes extends ImmutablePureComponent { const emptyMessage = <FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />; return ( - <Column icon='volume-off' heading={intl.formatMessage(messages.heading)}> + <Column bindToDocument={!multiColumn} icon='volume-off' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <ScrollableList scrollKey='mutes' diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index e708c4fcf..f2b239afe 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -198,7 +198,7 @@ class Notifications extends React.PureComponent { ); return ( - <Column ref={this.setColumnRef} label={intl.formatMessage(messages.title)}> + <Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.title)}> <ColumnHeader icon='bell' active={isUnread} diff --git a/app/javascript/mastodon/features/pinned_statuses/index.js b/app/javascript/mastodon/features/pinned_statuses/index.js index 64ebfc7ae..ad5c9cafc 100644 --- a/app/javascript/mastodon/features/pinned_statuses/index.js +++ b/app/javascript/mastodon/features/pinned_statuses/index.js @@ -47,7 +47,7 @@ class PinnedStatuses extends ImmutablePureComponent { const { intl, shouldUpdateScroll, statusIds, hasMore, multiColumn } = this.props; return ( - <Column icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}> + <Column bindToDocument={!multiColumn} icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}> <ColumnBackButtonSlim /> <StatusList statusIds={statusIds} diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js index 1edb303b8..e7825e236 100644 --- a/app/javascript/mastodon/features/public_timeline/index.js +++ b/app/javascript/mastodon/features/public_timeline/index.js @@ -105,7 +105,7 @@ class PublicTimeline extends React.PureComponent { const pinned = !!columnId; return ( - <Column ref={this.setRef} label={intl.formatMessage(messages.title)}> + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> <ColumnHeader icon='globe' active={hasUnread} diff --git a/app/javascript/mastodon/features/reblogs/index.js b/app/javascript/mastodon/features/reblogs/index.js index 26f93ad1b..229f626b3 100644 --- a/app/javascript/mastodon/features/reblogs/index.js +++ b/app/javascript/mastodon/features/reblogs/index.js @@ -51,7 +51,7 @@ class Reblogs extends ImmutablePureComponent { return ( <Column> - <ColumnBackButton /> + <ColumnBackButton multiColumn={multiColumn} /> <ScrollableList scrollKey='reblogs' diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 0422111ae..ad4f75820 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -146,6 +146,7 @@ class Status extends ImmutablePureComponent { descendantsIds: ImmutablePropTypes.list, intl: PropTypes.object.isRequired, askReplyConfirmation: PropTypes.bool, + multiColumn: PropTypes.bool, domain: PropTypes.string.isRequired, }; @@ -437,13 +438,13 @@ class Status extends ImmutablePureComponent { render () { let ancestors, descendants; - const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain } = this.props; + const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn } = this.props; const { fullscreen } = this.state; if (status === null) { return ( <Column> - <ColumnBackButton /> + <ColumnBackButton multiColumn={multiColumn} /> <MissingIndicator /> </Column> ); @@ -470,9 +471,10 @@ class Status extends ImmutablePureComponent { }; return ( - <Column label={intl.formatMessage(messages.detailedStatus)}> + <Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.detailedStatus)}> <ColumnHeader showBackButton + multiColumn={multiColumn} extraButton={( <button className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={status.get('hidden') ? 'false' : 'true'}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button> )} diff --git a/app/javascript/mastodon/features/ui/components/column_loading.js b/app/javascript/mastodon/features/ui/components/column_loading.js index 9503a7a1a..0cdfd05d8 100644 --- a/app/javascript/mastodon/features/ui/components/column_loading.js +++ b/app/javascript/mastodon/features/ui/components/column_loading.js @@ -21,7 +21,7 @@ export default class ColumnLoading extends ImmutablePureComponent { let { title, icon } = this.props; return ( <Column> - <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} /> + <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} placeholder /> <div className='scrollable' /> </Column> ); diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js index 29583d3d7..1911da8ba 100644 --- a/app/javascript/mastodon/features/ui/components/tabs_bar.js +++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -73,9 +73,13 @@ class TabsBar extends React.PureComponent { const { intl: { formatMessage } } = this.props; return ( - <nav className='tabs-bar' ref={this.setRef}> - {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))} - </nav> + <div className='tabs-bar__wrapper'> + <nav className='tabs-bar' ref={this.setRef}> + {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))} + </nav> + + <div id='tabs-bar__portal' /> + </div> ); } diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index e683a9c1a..7b0cdd5a5 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -153,9 +153,9 @@ const sortHashtagsByUse = (state, tags) => { if (usedA === usedB) { return 0; } else if (usedA && !usedB) { - return 1; - } else { return -1; + } else { + return 1; } }); }; diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss index 7df76bdff..7b983efab 100644 --- a/app/javascript/styles/mastodon/basics.scss +++ b/app/javascript/styles/mastodon/basics.scss @@ -39,7 +39,7 @@ body { &.layout-single-column { height: auto; - min-height: 100%; + min-height: 100vh; overflow-y: scroll; } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 1853cd2e5..97ef06efe 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -129,19 +129,28 @@ padding: 0; color: $action-button-color; border: 0; + border-radius: 4px; background: transparent; cursor: pointer; - transition: color 100ms ease-in; + transition: all 100ms ease-in; + transition-property: background-color, color; &:hover, &:active, &:focus { color: lighten($action-button-color, 7%); - transition: color 200ms ease-out; + background-color: rgba($action-button-color, 0.15); + transition: all 200ms ease-out; + transition-property: background-color, color; + } + + &:focus { + background-color: rgba($action-button-color, 0.3); } &.disabled { color: darken($action-button-color, 13%); + background-color: transparent; cursor: default; } @@ -166,10 +175,16 @@ &:active, &:focus { color: darken($lighter-text-color, 7%); + background-color: rgba($lighter-text-color, 0.15); + } + + &:focus { + background-color: rgba($lighter-text-color, 0.3); } &.disabled { color: lighten($lighter-text-color, 7%); + background-color: transparent; } &.active { @@ -197,6 +212,7 @@ .text-icon-button { color: $lighter-text-color; border: 0; + border-radius: 4px; background: transparent; cursor: pointer; font-weight: 600; @@ -204,17 +220,25 @@ padding: 0 3px; line-height: 27px; outline: 0; - transition: color 100ms ease-in; + transition: all 100ms ease-in; + transition-property: background-color, color; &:hover, &:active, &:focus { color: darken($lighter-text-color, 7%); - transition: color 200ms ease-out; + background-color: rgba($lighter-text-color, 0.15); + transition: all 200ms ease-out; + transition-property: background-color, color; + } + + &:focus { + background-color: rgba($lighter-text-color, 0.3); } &.disabled { color: lighten($lighter-text-color, 20%); + background-color: transparent; cursor: default; } @@ -604,7 +628,8 @@ } } - .icon-button { + .icon-button, + .text-icon-button { box-sizing: content-box; padding: 0 3px; } @@ -731,7 +756,7 @@ white-space: pre-wrap; &:last-child { - margin-bottom: 2px; + margin-bottom: 0; } } @@ -1852,6 +1877,26 @@ a.account__display-name { } } +.tabs-bar__wrapper { + background: darken($ui-base-color, 8%); + position: sticky; + top: 0; + z-index: 2; + padding-top: 0; + + @media screen and (min-width: $no-gap-breakpoint) { + padding-top: 10px; + } + + .tabs-bar { + margin-bottom: 0; + + @media screen and (min-width: $no-gap-breakpoint) { + margin-bottom: 10px; + } + } +} + .react-swipeable-view-container { &, .columns-area, @@ -1949,9 +1994,6 @@ a.account__display-name { background: lighten($ui-base-color, 8%); flex: 0 0 auto; overflow-y: auto; - position: sticky; - top: 0; - z-index: 3; } .tabs-bar__link { @@ -2014,6 +2056,14 @@ a.account__display-name { padding: 0; } + //.column { + // margin-top: 0; + + // @media screen and (min-width: $no-gap-breakpoint) { + // margin-top: 10px; + // } + //} + .autosuggest-textarea__textarea { font-size: 16px; } @@ -2039,6 +2089,7 @@ a.account__display-name { @media screen and (min-width: $no-gap-breakpoint) { padding: 10px 0; + padding-top: 0; } @media screen and (min-width: 630px) { @@ -2153,13 +2204,11 @@ a.account__display-name { @media screen and (min-width: $no-gap-breakpoint) { .tabs-bar { - margin: 10px auto; - margin-bottom: 0; width: 100%; } .react-swipeable-view-container .columns-area--mobile { - height: calc(100% - 20px) !important; + height: calc(100% - 10px) !important; } .getting-started__wrapper, @@ -2387,6 +2436,8 @@ a.account__display-name { } .column-back-button { + box-sizing: border-box; + width: 100%; background: lighten($ui-base-color, 4%); color: $highlight-text-color; cursor: pointer; diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 59767cdfe..224d90660 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -37,7 +37,7 @@ class FeedManager end def unpush_from_home(account, status) - return false unless remove_from_feed(:home, account.id, status) + return false unless remove_from_feed(:home, account.id, status, account.user&.aggregates_reblogs?) redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) true end @@ -56,7 +56,7 @@ class FeedManager end def unpush_from_list(list, status) - return false unless remove_from_feed(:list, list.id, status) + return false unless remove_from_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?) redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) true end @@ -120,7 +120,7 @@ class FeedManager oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0 from_account.statuses.select('id, reblog_of_id').where('id > ?', oldest_home_score).reorder(nil).find_each do |status| - remove_from_feed(:home, into_account.id, status) + remove_from_feed(:home, into_account.id, status, into_account.user&.aggregates_reblogs?) end end @@ -316,10 +316,11 @@ class FeedManager # with reblogs, and returning true if a status was removed. As with # `add_to_feed`, this does not trigger push updates, so callers must # do so if appropriate. - def remove_from_feed(timeline_type, account_id, status) + def remove_from_feed(timeline_type, account_id, status, aggregate_reblogs = true) timeline_key = key(timeline_type, account_id) + reblog_key = key(timeline_type, account_id, 'reblogs') - if status.reblog? + if status.reblog? && (aggregate_reblogs.nil? || aggregate_reblogs) # 1. If the reblogging status is not in the feed, stop. status_rank = redis.zrevrank(timeline_key, status.id) return false if status_rank.nil? @@ -328,6 +329,7 @@ class FeedManager reblog_set_key = key(timeline_type, account_id, "reblogs:#{status.reblog_of_id}") redis.srem(reblog_set_key, status.id) + redis.zrem(reblog_key, status.reblog_of_id) # 3. Re-insert another reblog or original into the feed if one # remains in the set. We could pick a random element, but this # set should generally be small, and it seems ideal to show the @@ -335,12 +337,14 @@ class FeedManager other_reblog = redis.smembers(reblog_set_key).map(&:to_i).min redis.zadd(timeline_key, other_reblog, other_reblog) if other_reblog + redis.zadd(reblog_key, other_reblog, status.reblog_of_id) if other_reblog # 4. Remove the reblogging status from the feed (as normal) # (outside conditional) else # If the original is getting deleted, no use for reblog references redis.del(key(timeline_type, account_id, "reblogs:#{status.id}")) + redis.zrem(reblog_key, status.id) end redis.zrem(timeline_key, status.id) diff --git a/app/models/tag.rb b/app/models/tag.rb index 46e3a3ec0..c7f0af86d 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -7,6 +7,7 @@ # name :string default(""), not null # created_at :datetime not null # updated_at :datetime not null +# score :integer # class Tag < ApplicationRecord @@ -75,10 +76,12 @@ class Tag < ApplicationRecord end def search_for(term, limit = 5, offset = 0) - pattern = sanitize_sql_like(normalize(term.strip)) + '%' + normalized_term = normalize(term.strip).mb_chars.downcase.to_s + pattern = sanitize_sql_like(normalized_term) + '%' - Tag.where(arel_table[:name].lower.matches(pattern.mb_chars.downcase.to_s)) - .order(:name) + Tag.where(arel_table[:name].lower.matches(pattern)) + .where(arel_table[:score].gt(0).or(arel_table[:name].lower.eq(normalized_term))) + .order(Arel.sql('length(name) ASC, score DESC, name ASC')) .limit(limit) .offset(offset) end diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb index 148535c21..211c8f1dc 100644 --- a/app/models/trending_tags.rb +++ b/app/models/trending_tags.rb @@ -48,12 +48,17 @@ class TrendingTags redis.zrem(key, tag_id.to_s) else score = ((observed - expected)**2) / expected - redis.zadd(key, score, tag_id.to_s) + added = redis.zadd(key, score, tag_id.to_s) + bump_tag_score!(tag_id) if added end redis.expire(key, EXPIRE_TRENDS_AFTER) end + def bump_tag_score!(tag_id) + Tag.where(id: tag_id).update_all('score = COALESCE(score, 0) + 1') + end + def disallowed_hashtags return @disallowed_hashtags if defined?(@disallowed_hashtags) diff --git a/app/serializers/webfinger_serializer.rb b/app/serializers/webfinger_serializer.rb index 008d0c182..c67363b8f 100644 --- a/app/serializers/webfinger_serializer.rb +++ b/app/serializers/webfinger_serializer.rb @@ -26,7 +26,6 @@ class WebfingerSerializer < ActiveModel::Serializer else [ { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) }, - { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') }, { rel: 'self', type: 'application/activity+json', href: account_url(object) }, { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" }, ] diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index 6846abeb6..034304936 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -7,7 +7,6 @@ - if @account.user&.setting_noindex %meta{ name: 'robots', content: 'noindex, noarchive' }/ - %link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/ %link{ rel: 'alternate', type: 'application/rss+xml', href: account_url(@account, format: 'rss') }/ %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/ @@ -74,7 +73,7 @@ - if featured_tag.last_status_at.nil? = t('accounts.nothing_here') - else - %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at + %time.formatted{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true = render 'application/sidebar' diff --git a/app/views/well_known/webfinger/show.xml.ruby b/app/views/well_known/webfinger/show.xml.ruby deleted file mode 100644 index f5a54052a..000000000 --- a/app/views/well_known/webfinger/show.xml.ruby +++ /dev/null @@ -1,51 +0,0 @@ -doc = Ox::Document.new(version: '1.0') - -doc << Ox::Element.new('XRD').tap do |xrd| - xrd['xmlns'] = 'http://docs.oasis-open.org/ns/xri/xrd-1.0' - - xrd << (Ox::Element.new('Subject') << @account.to_webfinger_s) - - if @account.instance_actor? - xrd << (Ox::Element.new('Alias') << instance_actor_url) - - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'http://webfinger.net/rel/profile-page' - link['type'] = 'text/html' - link['href'] = about_more_url(instance_actor: true) - end - - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'self' - link['type'] = 'application/activity+json' - link['href'] = instance_actor_url - end - else - xrd << (Ox::Element.new('Alias') << short_account_url(@account)) - xrd << (Ox::Element.new('Alias') << account_url(@account)) - - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'http://webfinger.net/rel/profile-page' - link['type'] = 'text/html' - link['href'] = short_account_url(@account) - end - - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'http://schemas.google.com/g/2010#updates-from' - link['type'] = 'application/atom+xml' - link['href'] = account_url(@account, format: 'atom') - end - - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'self' - link['type'] = 'application/activity+json' - link['href'] = account_url(@account) - end - - xrd << Ox::Element.new('Link').tap do |link| - link['rel'] = 'http://ostatus.org/schema/1.0/subscribe' - link['template'] = "#{authorize_interaction_url}?acct={uri}" - end - end -end - -('<?xml version="1.0" encoding="UTF-8"?>' + Ox.dump(doc, effort: :tolerant)).force_encoding('UTF-8') |