Merge branch 'master' into glitch-soc/merge-upstream
This commit is contained in:
@ -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
|
||||
|
@ -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 () {
|
||||
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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}>
|
||||
|
@ -100,7 +100,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton />
|
||||
<ColumnBackButton multiColumn={multiColumn} />
|
||||
|
||||
<StatusList
|
||||
prepend={<HeaderContainer accountId={this.props.params.accountId} />}
|
||||
|
@ -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'
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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'
|
||||
|
@ -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)}
|
||||
|
@ -51,7 +51,7 @@ class Favourites extends ImmutablePureComponent {
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton />
|
||||
<ColumnBackButton multiColumn={multiColumn} />
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='favourites'
|
||||
|
@ -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'
|
||||
|
@ -78,7 +78,7 @@ class Followers extends ImmutablePureComponent {
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton />
|
||||
<ColumnBackButton multiColumn={multiColumn} />
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='followers'
|
||||
|
@ -78,7 +78,7 @@ class Following extends ImmutablePureComponent {
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton />
|
||||
<ColumnBackButton multiColumn={multiColumn} />
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='following'
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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 => ({
|
||||
|
@ -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}
|
||||
|
@ -66,7 +66,7 @@ class NewListForm extends React.PureComponent {
|
||||
</label>
|
||||
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
disabled={disabled || !value}
|
||||
icon='plus'
|
||||
title={title}
|
||||
onClick={this.handleClick}
|
||||
|
@ -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 />
|
||||
|
@ -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'
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -51,7 +51,7 @@ class Reblogs extends ImmutablePureComponent {
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton />
|
||||
<ColumnBackButton multiColumn={multiColumn} />
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='reblogs'
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -39,7 +39,7 @@ body {
|
||||
|
||||
&.layout-single-column {
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
min-height: 100vh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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}" },
|
||||
]
|
||||
|
@ -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'
|
||||
|
@ -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')
|
Reference in New Issue
Block a user