Merge branch 'next' into 'locked-profile-header'

# Conflicts:
#   app/soapbox/features/account/components/header.js
#   app/soapbox/pages/profile_page.js
merge-requests/692/head
marcin mikołajczak 2021-10-09 22:00:01 +00:00
commit be5203f406
66 zmienionych plików z 682 dodań i 517 usunięć

Wyświetl plik

@ -25,7 +25,7 @@ exports[`<TimelineQueueButtonHeader /> renders correctly 1`] = `
exports[`<TimelineQueueButtonHeader /> renders correctly 2`] = `
<div
className="timeline-queue-header"
className="timeline-queue-header hidden"
>
<a
className="timeline-queue-header__btn"
@ -42,14 +42,18 @@ exports[`<TimelineQueueButtonHeader /> renders correctly 2`] = `
}
/>
</div>
Click to see 1 new post
<div
className="timeline-queue-header__label"
>
Click to see 1 new post
</div>
</a>
</div>
`;
exports[`<TimelineQueueButtonHeader /> renders correctly 3`] = `
<div
className="timeline-queue-header"
className="timeline-queue-header hidden"
>
<a
className="timeline-queue-header__btn"
@ -66,7 +70,11 @@ exports[`<TimelineQueueButtonHeader /> renders correctly 3`] = `
}
/>
</div>
Click to see 9999999 new posts
<div
className="timeline-queue-header__label"
>
Click to see 9999999 new posts
</div>
</a>
</div>
`;

Wyświetl plik

@ -0,0 +1,49 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
import { openModal } from 'soapbox/actions/modal';
import Bundle from 'soapbox/features/ui/components/bundle';
export default @connect()
class AttachmentThumbs extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
media: ImmutablePropTypes.list.isRequired,
onClick: PropTypes.func,
};
renderLoading() {
return <div className='media-gallery--compact' />;
}
onOpenMedia = (media, index) => {
this.props.dispatch(openModal('MEDIA', { media, index }));
}
render() {
const { media, onClick } = this.props;
return (
<div className='attachment-thumbs'>
<Bundle fetchComponent={MediaGallery} loading={this.renderLoading}>
{Component => (
<Component
media={media}
onOpenMedia={this.onOpenMedia}
height={50}
compact
/>
)}
</Bundle>
{onClick && (
<div className='attachment-thumbs__clickable-region' onClick={onClick} />
)}
</div>
);
}
}

Wyświetl plik

@ -1,185 +0,0 @@
'use strict';
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import classNames from 'classnames';
import { injectIntl, defineMessages } from 'react-intl';
import { Link } from 'react-router-dom';
import Icon from 'soapbox/components/icon';
import { fetchLists } from 'soapbox/actions/lists';
import { createSelector } from 'reselect';
import { getFeatures } from 'soapbox/utils/features';
const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
homeTitle: { id: 'home_column_header.home', defaultMessage: 'Home' },
allTitle: { id: 'home_column_header.all', defaultMessage: 'All' },
fediverseTitle: { id: 'home_column_header.fediverse', defaultMessage: 'Fediverse' },
listTitle: { id: 'home_column.lists', defaultMessage: 'Lists' },
});
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
if (!lists) {
return lists;
}
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
});
const mapStateToProps = state => {
const instance = state.get('instance');
const features = getFeatures(instance);
return {
lists: getOrderedLists(state),
siteTitle: state.getIn(['instance', 'title']),
federating: features.federating,
};
};
class ColumnHeader extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
intl: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
active: PropTypes.bool,
children: PropTypes.node,
activeItem: PropTypes.string,
activeSubItem: PropTypes.string,
lists: ImmutablePropTypes.list,
siteTitle: PropTypes.string,
federating: PropTypes.bool,
};
state = {
collapsed: true,
animating: false,
expandedFor: null, //lists, groups, etc.
};
componentDidMount() {
this.props.dispatch(fetchLists());
}
handleToggleClick = (e) => {
e.stopPropagation();
this.setState({ collapsed: !this.state.collapsed, animating: true });
}
handleTransitionEnd = () => {
this.setState({ animating: false });
}
expandLists = () => {
this.setState({
expandedFor: 'lists',
});
}
render() {
const { active, children, intl: { formatMessage }, activeItem, activeSubItem, lists, siteTitle, federating } = this.props;
const { collapsed, animating, expandedFor } = this.state;
const wrapperClassName = classNames('column-header__wrapper', {
'active': active,
});
const buttonClassName = classNames('column-header', {
'active': active,
});
const collapsibleClassName = classNames('column-header__collapsible', {
'collapsed': collapsed,
'animating': animating,
});
const collapsibleButtonClassName = classNames('column-header__button', {
'active': !collapsed,
});
const expansionClassName = classNames('column-header column-header__expansion', {
'open': expandedFor,
});
let extraContent, collapseButton;
if (children) {
extraContent = (
<div key='extra-content' className='column-header__collapsible__extra'>
{children}
</div>
);
collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>;
}
const collapsedContent = [
extraContent,
];
let expandedContent = null;
if ((expandedFor === 'lists' || activeItem === 'lists') && lists) {
expandedContent = lists.map(list =>
(<Link
key={list.get('id')}
to={`/list/${list.get('id')}`}
className={
classNames('btn btn--sub grouped', {
'active': list.get('id') === activeSubItem,
})
}
>
{list.get('title')}
</Link>),
);
}
return (
<div className={wrapperClassName}>
<h1 className={buttonClassName}>
<Link to='/' className={classNames('btn grouped', { 'active': 'home' === activeItem })}>
<Icon id='home' fixedWidth className='column-header__icon' />
{formatMessage(messages.homeTitle)}
</Link>
<Link to='/timeline/local' className={classNames('btn grouped', { 'active': 'local' === activeItem })}>
<Icon id={federating ? 'users' : 'globe-w'} fixedWidth className='column-header__icon' />
{federating ? siteTitle : formatMessage(messages.allTitle)}
</Link>
{federating && <Link to='/timeline/fediverse' className={classNames('btn grouped', { 'active': 'fediverse' === activeItem })}>
<Icon id='fediverse' fixedWidth className='column-header__icon' />
{formatMessage(messages.fediverseTitle)}
</Link>}
<div className='column-header__buttons'>
{collapseButton}
</div>
</h1>
{
expandedContent &&
<h1 className={expansionClassName}>
{expandedContent}
</h1>
}
<div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}>
<div className='column-header__collapsible-inner'>
{(!collapsed || animating) && collapsedContent}
</div>
</div>
</div>
);
}
}
export default injectIntl(connect(mapStateToProps)(ColumnHeader));

Wyświetl plik

@ -19,9 +19,9 @@ export default class MaterialStatus extends React.Component {
}
return (
<div className='material-status'>
<div className='material-status__status'>
<StatusContainer {...this.props} />
<div className='material-status' tabIndex={-1}>
<div className='material-status__status focusable' tabIndex={0}>
<StatusContainer {...this.props} focusable={false} />
</div>
</div>
);

Wyświetl plik

@ -279,6 +279,7 @@ class MediaGallery extends React.PureComponent {
visible: PropTypes.bool,
onToggleVisibility: PropTypes.func,
displayMedia: PropTypes.string,
compact: PropTypes.bool,
};
static defaultProps = {
@ -560,7 +561,7 @@ class MediaGallery extends React.PureComponent {
}
render() {
const { media, intl, sensitive } = this.props;
const { media, intl, sensitive, compact } = this.props;
const { visible } = this.state;
const sizeData = this.getSizeData(media.size);
@ -592,7 +593,7 @@ class MediaGallery extends React.PureComponent {
}
return (
<div className='media-gallery' style={sizeData.get('style')} ref={this.handleRef}>
<div className={classNames('media-gallery', { 'media-gallery--compact': compact })} style={sizeData.get('style')} ref={this.handleRef}>
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}>
{spoilerButton}
</div>

Wyświetl plik

@ -8,7 +8,7 @@ import RelativeTimestamp from './relative_timestamp';
import DisplayName from './display_name';
import StatusContent from './status_content';
import StatusActionBar from './status_action_bar';
import AttachmentList from './attachment_list';
import AttachmentThumbs from './attachment_thumbs';
import Card from '../features/status/components/card';
import { injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
@ -93,6 +93,11 @@ class Status extends ImmutablePureComponent {
group: ImmutablePropTypes.map,
displayMedia: PropTypes.string,
allowedEmoji: ImmutablePropTypes.list,
focusable: PropTypes.bool,
};
static defaultProps = {
focusable: true,
};
// Avoid checking props that are functions (and whose equality will always
@ -336,7 +341,7 @@ class Status extends ImmutablePureComponent {
return (
<HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
<div className={classNames('status__wrapper', 'status__wrapper--filtered', { focusable: this.props.focusable })} tabIndex={this.props.focusable ? 0 : null} ref={this.handleRef}>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
</div>
</HotKeys>
@ -384,10 +389,7 @@ class Status extends ImmutablePureComponent {
if (size > 0) {
if (this.props.muted) {
media = (
<AttachmentList
compact
media={status.get('media_attachments')}
/>
<AttachmentThumbs media={status.get('media_attachments')} onClick={this.handleClick} />
);
} else if (size === 1 && status.getIn(['media_attachments', 0, 'type']) === 'video') {
const video = status.getIn(['media_attachments', 0]);
@ -493,7 +495,7 @@ class Status extends ImmutablePureComponent {
return (
<HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: this.props.focusable && !this.props.muted })} tabIndex={this.props.focusable && !this.props.muted ? 0 : null} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
{prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>

Wyświetl plik

@ -458,7 +458,13 @@ class StatusActionBar extends ImmutablePureComponent {
emoji={meEmojiReact}
onClick={this.handleLikeButtonClick}
/>
{emojiReactCount !== 0 && <span className='detailed-status__link'>{emojiReactCount}</span>}
{emojiReactCount !== 0 && (
(features.exposableReactions && !features.emojiReacts) ? (
<Link to={`/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}/likes`} className='detailed-status__link'>{emojiReactCount}</Link>
) : (
<span className='detailed-status__link'>{emojiReactCount}</span>
)
)}
</div>
{shareButton}

Wyświetl plik

@ -82,7 +82,6 @@ export default class StatusList extends ImmutablePureComponent {
}
handleDequeueTimeline = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
const { onDequeueTimeline, timelineId } = this.props;
if (!onDequeueTimeline || !timelineId) return;
onDequeueTimeline(timelineId);

Wyświetl plik

@ -1,7 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { throttle } from 'lodash';
import Icon from 'soapbox/components/icon';
import classNames from 'classnames';
export default class SubNavigation extends React.PureComponent {
@ -13,6 +15,10 @@ export default class SubNavigation extends React.PureComponent {
router: PropTypes.object.isRequired,
}
state = {
scrolled: false,
}
handleBackClick = () => {
if (window.history && window.history.length === 1) {
this.context.router.history.push('/');
@ -27,11 +33,44 @@ export default class SubNavigation extends React.PureComponent {
}
}
componentDidMount() {
this.attachScrollListener();
}
componentWillUnmount() {
this.detachScrollListener();
}
attachScrollListener() {
window.addEventListener('scroll', this.handleScroll);
}
detachScrollListener() {
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll = throttle(() => {
if (this.node) {
const { top } = this.node.getBoundingClientRect();
if (top <= 50) {
this.setState({ scrolled: true });
} else {
this.setState({ scrolled: false });
}
}
}, 150, { trailing: true });
setRef = c => {
this.node = c;
}
render() {
const { message } = this.props;
const { scrolled } = this.state;
return (
<div className='sub-navigation'>
<div className={classNames('sub-navigation', { 'sub-navigation--scrolled': scrolled })} ref={this.setRef}>
<div className='sub-navigation__content'>
<button
className='sub-navigation__back'

Wyświetl plik

@ -1,8 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl } from 'react-intl';
import { throttle } from 'lodash';
import classNames from 'classnames';
import SvgIcon from 'soapbox/components/svg_icon';
import Icon from 'soapbox/components/icon';
export default @injectIntl
class TimelineQueueButtonHeader extends React.PureComponent {
@ -11,25 +12,87 @@ class TimelineQueueButtonHeader extends React.PureComponent {
onClick: PropTypes.func.isRequired,
count: PropTypes.number,
message: PropTypes.object.isRequired,
threshold: PropTypes.number,
intl: PropTypes.object.isRequired,
autoload: PropTypes.bool,
autoloadThreshold: PropTypes.number,
};
static defaultProps = {
count: 0,
threshold: 400,
autoload: true,
autoloadThreshold: 50,
};
state = {
scrolled: false,
}
componentDidMount() {
this.attachScrollListener();
}
componentWillUnmount() {
this.detachScrollListener();
}
componentDidUpdate(prevProps, prevState) {
const { scrollTop } = (document.scrollingElement || document.documentElement);
const { count, onClick, autoload, autoloadThreshold } = this.props;
if (autoload && scrollTop <= autoloadThreshold && count !== prevProps.count) {
onClick();
}
}
attachScrollListener() {
window.addEventListener('scroll', this.handleScroll);
}
detachScrollListener() {
window.removeEventListener('scroll', this.handleScroll);
}
handleScroll = throttle(() => {
const { scrollTop } = (document.scrollingElement || document.documentElement);
const { threshold, onClick, autoload, autoloadThreshold } = this.props;
if (autoload && scrollTop <= autoloadThreshold) {
onClick();
}
if (scrollTop > threshold) {
this.setState({ scrolled: true });
} else {
this.setState({ scrolled: false });
}
}, 150, { trailing: true });
handleClick = e => {
window.scrollTo({ top: 0, behavior: 'smooth' });
this.props.onClick(e);
}
render() {
const { count, message, onClick, intl } = this.props;
const { count, message, intl } = this.props;
const { scrolled } = this.state;
const visible = count > 0 && scrolled;
const classes = classNames('timeline-queue-header', {
'hidden': (count <= 0),
'hidden': !visible,
});
return (
<div className={classes}>
<a className='timeline-queue-header__btn' onClick={onClick}>
<SvgIcon src={require('@tabler/icons/icons/arrow-bar-to-up.svg')} />
{(count > 0) && intl.formatMessage(message, { count })}
<a className='timeline-queue-header__btn' onClick={this.handleClick}>
<Icon src={require('@tabler/icons/icons/arrow-bar-to-up.svg')} />
{(count > 0) && (
<div className='timeline-queue-header__label'>
{intl.formatMessage(message, { count })}
</div>
)}
</a>
</div>
);

Wyświetl plik

@ -20,6 +20,8 @@ import classNames from 'classnames';
import Avatar from 'soapbox/components/avatar';
import { getAcct } from 'soapbox/utils/accounts';
import { displayFqn } from 'soapbox/utils/state';
import { shortNumberFormat } from 'soapbox/utils/numbers';
import { NavLink } from 'react-router-dom';
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import { ProfileInfoPanel } from 'soapbox/features/ui/util/async-components';
@ -346,11 +348,12 @@ class Header extends ImmutablePureComponent {
);
}
const ownAccount = account.get('id') === me;
const info = this.makeInfo();
const menu = this.makeMenu();
const header = account.get('header', '');
const headerMissing = !header || ['/images/banner.png', '/headers/original/missing.png'].some(path => header.endsWith(path));
// const headerMissing = !header || ['/images/banner.png', '/headers/original/missing.png'].some(path => header.endsWith(path));
const avatarSize = isSmallScreen ? 90 : 200;
const deactivated = !account.getIn(['pleroma', 'is_active'], true);
@ -360,7 +363,7 @@ class Header extends ImmutablePureComponent {
return (
<div className={classNames('account__header', { inactive: !!account.get('moved'), deactivated: deactivated })}>
<div className={classNames('account__header__image', { 'account__header__image--none': headerMissing || deactivated })}>
<div className={classNames('account__header__image', { /* 'account__header__image--none': headerMissing || deactivated */ })}>
<div className='account__header__info'>
{info}
</div>
@ -392,6 +395,40 @@ class Header extends ImmutablePureComponent {
</div>
</div>
<div className='account__header__extra__links'>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<span>{shortNumberFormat(account.get('statuses_count'))}</span>
<span><FormattedMessage id='account.posts' defaultMessage='Posts' /></span>
</NavLink>
{(ownAccount || !account.getIn(['pleroma', 'hide_follows'], false)) && <NavLink exact activeClassName='active' to={`/@${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}>
{account.getIn(['pleroma', 'hide_follows_count'], false) ? <span></span> : <span>{shortNumberFormat(account.get('following_count'))}</span>}
<span><FormattedMessage id='account.follows' defaultMessage='Follows' /></span>
</NavLink>}
{(ownAccount || !account.getIn(['pleroma', 'hide_followers'], false)) && <NavLink exact activeClassName='active' to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
{account.getIn(['pleroma', 'hide_followers_count'], false) ? <span></span> : <span>{shortNumberFormat(account.get('followers_count'))}</span>}
<span><FormattedMessage id='account.followers' defaultMessage='Followers' /></span>
</NavLink>}
{(ownAccount || !account.getIn(['pleroma', 'hide_favorites'], true)) && <NavLink exact activeClassName='active' to={`/@${account.get('acct')}/favorites`}>
{ /* : TODO : shortNumberFormat(account.get('favourite_count')) */ }
<span></span>
<span><FormattedMessage id='navigation_bar.favourites' defaultMessage='Likes' /></span>
</NavLink>}
{ownAccount &&
<NavLink
exact activeClassName='active' to={`/@${account.get('acct')}/pins`}
>
{ /* : TODO : shortNumberFormat(account.get('pinned_count')) */ }
<span></span>
<span><FormattedMessage id='navigation_bar.pins' defaultMessage='Pins' /></span>
</NavLink>
}
</div>
{isSmallScreen && (
<div className={classNames('account-mobile-container', { 'deactivated': deactivated })}>
<BundleContainer fetchComponent={ProfileInfoPanel}>

Wyświetl plik

@ -10,7 +10,7 @@ import { expandAccountMediaTimeline } from '../../actions/timelines';
import LoadingIndicator from 'soapbox/components/loading_indicator';
import Column from '../ui/components/column';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { getAccountGallery } from 'soapbox/selectors';
import { getAccountGallery, findAccountByUsername } from 'soapbox/selectors';
import MediaItem from './components/media_item';
import LoadMore from 'soapbox/components/load_more';
import MissingIndicator from 'soapbox/components/missing_indicator';
@ -21,7 +21,6 @@ import { FormattedMessage } from 'react-intl';
const mapStateToProps = (state, { params, withReplies = false }) => {
const username = params.username || '';
const me = state.get('me');
const accounts = state.getIn(['accounts']);
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
let accountId = -1;
@ -29,7 +28,7 @@ const mapStateToProps = (state, { params, withReplies = false }) => {
if (accountFetchError) {
accountId = null;
} else {
const account = accounts.find(acct => username.toLowerCase() === acct.getIn(['acct'], '').toLowerCase());
const account = findAccountByUsername(state, username);
accountId = account ? account.getIn(['id'], null) : -1;
accountUsername = account ? account.getIn(['acct'], '') : '';
}

Wyświetl plik

@ -18,7 +18,7 @@ import { NavLink } from 'react-router-dom';
import { fetchPatronAccount } from '../../actions/patron';
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import { getSettings } from 'soapbox/actions/settings';
import { makeGetStatusIds } from 'soapbox/selectors';
import { makeGetStatusIds, findAccountByUsername } from 'soapbox/selectors';
import classNames from 'classnames';
const makeMapStateToProps = () => {
@ -27,7 +27,6 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, { params, withReplies = false }) => {
const username = params.username || '';
const me = state.get('me');
const accounts = state.getIn(['accounts']);
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
const soapboxConfig = getSoapboxConfig(state);
@ -37,7 +36,7 @@ const makeMapStateToProps = () => {
if (accountFetchError) {
accountId = null;
} else {
const account = accounts.find(acct => username.toLowerCase() === acct.getIn(['acct'], '').toLowerCase());
const account = findAccountByUsername(state, username);
accountId = account ? account.getIn(['id'], null) : -1;
accountUsername = account ? account.getIn(['acct'], '') : '';
accountApId = account ? account.get('url') : '';

Wyświetl plik

@ -103,7 +103,6 @@ class UserIndex extends ImmutablePureComponent {
<Column>
<SimpleForm style={{ paddingBottom: 0 }}>
<TextInput
value={this.state.q}
onChange={this.handleQueryChange}
placeholder={intl.formatMessage(messages.searchPlaceholder)}
/>

Wyświetl plik

@ -171,9 +171,8 @@ class ChatBox extends ImmutablePureComponent {
return this.canSubmit() ? (
<div className='chat-box__send'>
<IconButton
icon='send'
src={require('@tabler/icons/icons/send.svg')}
title={intl.formatMessage(messages.send)}
size={16}
onClick={this.sendMessage}
/>
</div>

Wyświetl plik

@ -1,5 +1,6 @@
import React from 'react';
import CharacterCounter from './character_counter';
// import TextCharacterCounter from './text_character_counter';
import VisualCharacterCounter from './visual_character_counter';
import Button from '../../../components/button';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
@ -351,7 +352,12 @@ export default class ComposeForm extends ImmutablePureComponent {
<SpoilerButtonContainer />
<MarkdownButtonContainer />
</div>
{maxTootChars && <div className='character-counter__wrapper'><CharacterCounter max={maxTootChars} text={text} /></div>}
{maxTootChars && (
<div className='compose-form__counter'>
{/* <TextCharacterCounter max={maxTootChars} text={text} /> */}
<VisualCharacterCounter max={maxTootChars} text={text} />
</div>
)}
<div className='compose-form__publish'>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabledButton} block /></div>
</div>

Wyświetl plik

@ -7,7 +7,7 @@ import DisplayName from '../../../components/display_name';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { isRtl } from '../../../rtl';
import AttachmentList from 'soapbox/components/attachment_list';
import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
import { NavLink } from 'react-router-dom';
const messages = defineMessages({
@ -57,7 +57,7 @@ class ReplyIndicator extends ImmutablePureComponent {
<div className='reply-indicator__content' style={style} dangerouslySetInnerHTML={content} />
{status.get('media_attachments').size > 0 && (
<AttachmentList
<AttachmentThumbs
compact
media={status.get('media_attachments')}
/>

Wyświetl plik

@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import { length } from 'stringz';
export default class TextCharacterCounter extends React.PureComponent {
static propTypes = {
text: PropTypes.string.isRequired,
max: PropTypes.number.isRequired,
};
checkRemainingText(diff) {
if (diff < 0) {
return <span className='character-counter character-counter--over'>{diff}</span>;
}
return <span className='character-counter'>{diff}</span>;
}
render() {
const diff = this.props.max - length(this.props.text);
return this.checkRemainingText(diff);
}
}

Wyświetl plik

@ -13,7 +13,7 @@ const messages = defineMessages({
* @param {string} props.text - text to use to measure
* @param {number} props.max - max text allowed
*/
class CharacterCounter extends React.PureComponent {
class VisualCharacterCounter extends React.PureComponent {
render() {
const { intl, text, max } = this.props;
@ -22,21 +22,23 @@ class CharacterCounter extends React.PureComponent {
const progress = textLength / max;
return (
<ProgressCircle
title={intl.formatMessage(messages.title, { chars: textLength, maxChars: max })}
progress={progress}
radius={10}
stroke={3}
/>
<div className='visual-character-counter'>
<ProgressCircle
title={intl.formatMessage(messages.title, { chars: textLength, maxChars: max })}
progress={progress}
radius={10}
stroke={3}
/>
</div>
);
}
}
CharacterCounter.propTypes = {
VisualCharacterCounter.propTypes = {
intl: PropTypes.object.isRequired,
text: PropTypes.string.isRequired,
max: PropTypes.number.isRequired,
};
export default injectIntl(CharacterCounter);
export default injectIntl(VisualCharacterCounter);

Wyświetl plik

@ -6,30 +6,41 @@ import {
showSearch,
} from '../../../actions/search';
import Search from '../components/search';
import { debounce } from 'lodash';
const mapStateToProps = state => ({
value: state.getIn(['search', 'value']),
submitted: state.getIn(['search', 'submitted']),
});
const mapDispatchToProps = dispatch => ({
const mapDispatchToProps = (dispatch, { autoSubmit }) => {
onChange(value) {
dispatch(changeSearch(value));
},
onClear() {
dispatch(clearSearch());
},
onSubmit() {
const debouncedSubmit = debounce(() => {
dispatch(submitSearch());
},
}, 900);
onShow() {
dispatch(showSearch());
},
return {
onChange(value) {
dispatch(changeSearch(value));
});
if (autoSubmit) {
debouncedSubmit();
}
},
onClear() {
dispatch(clearSearch());
},
onSubmit() {
dispatch(submitSearch());
},
onShow() {
dispatch(showSearch());
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Search);

Wyświetl plik

@ -2,6 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router-dom';
import { getAcct, isVerified } from 'soapbox/utils/accounts';
import StillImage from 'soapbox/components/still_image';
import VerificationBadge from 'soapbox/components/verification_badge';
@ -13,7 +14,7 @@ const mapStateToProps = state => ({
const ProfilePreview = ({ account, displayFqn }) => (
<div className='card h-card'>
<a target='_blank' rel='noopener' href={account.get('url')}>
<Link to={`/@${account.get('acct')}`}>
<div className='card__img'>
<StillImage alt='' src={account.get('header')} />
</div>
@ -32,7 +33,7 @@ const ProfilePreview = ({ account, displayFqn }) => (
<span>@{getAcct(account, displayFqn)}</span>
</div>
</div>
</a>
</Link>
</div>
);

Wyświetl plik

@ -11,6 +11,7 @@ import { debounce } from 'lodash';
import MissingIndicator from 'soapbox/components/missing_indicator';
import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts';
import LoadingIndicator from '../../components/loading_indicator';
import { findAccountByUsername } from 'soapbox/selectors';
const mapStateToProps = (state, { params }) => {
const username = params.username || '';
@ -28,14 +29,13 @@ const mapStateToProps = (state, { params }) => {
};
}
const accounts = state.getIn(['accounts']);
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
let accountId = -1;
if (accountFetchError) {
accountId = null;
} else {
const account = accounts.find(acct => username.toLowerCase() === acct.getIn(['acct'], '').toLowerCase());
const account = findAccountByUsername(state, username);
accountId = account ? account.getIn(['id'], null) : -1;
}

Wyświetl plik

@ -5,19 +5,25 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { fetchFavourites } from '../../actions/interactions';
import { FormattedMessage } from 'react-intl';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import ScrollableList from '../../components/scrollable_list';
const messages = defineMessages({
heading: { id: 'column.favourites', defaultMessage: 'Likes' },
});
const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
});
export default @connect(mapStateToProps)
@injectIntl
class Favourites extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.orderedSet,
@ -37,7 +43,7 @@ class Favourites extends ImmutablePureComponent {
}
render() {
const { accountIds } = this.props;
const { intl, accountIds } = this.props;
if (!accountIds) {
return (
@ -50,7 +56,7 @@ class Favourites extends ImmutablePureComponent {
const emptyMessage = <FormattedMessage id='empty_column.favourites' defaultMessage='No one has liked this post yet. When someone does, they will show up here.' />;
return (
<Column>
<Column heading={intl.formatMessage(messages.heading)}>
<ScrollableList
scrollKey='favourites'
emptyMessage={emptyMessage}

Wyświetl plik

@ -17,18 +17,18 @@ import Column from '../ui/components/column';
import ScrollableList from '../../components/scrollable_list';
import MissingIndicator from 'soapbox/components/missing_indicator';
import { getFollowDifference } from 'soapbox/utils/accounts';
import { findAccountByUsername } from 'soapbox/selectors';
const mapStateToProps = (state, { params, withReplies = false }) => {
const username = params.username || '';
const me = state.get('me');
const accounts = state.getIn(['accounts']);
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
let accountId = -1;
if (accountFetchError) {
accountId = null;
} else {
const account = accounts.find(acct => username.toLowerCase() === acct.getIn(['acct'], '').toLowerCase());
const account = findAccountByUsername(state, username);
accountId = account ? account.getIn(['id'], null) : -1;
}

Wyświetl plik

@ -17,18 +17,18 @@ import Column from '../ui/components/column';
import ScrollableList from '../../components/scrollable_list';
import MissingIndicator from 'soapbox/components/missing_indicator';
import { getFollowDifference } from 'soapbox/utils/accounts';
import { findAccountByUsername } from 'soapbox/selectors';
const mapStateToProps = (state, { params, withReplies = false }) => {
const username = params.username || '';
const me = state.get('me');
const accounts = state.getIn(['accounts']);
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
let accountId = -1;
if (accountFetchError) {
accountId = null;
} else {
const account = accounts.find(acct => username.toLowerCase() === acct.getIn(['acct'], '').toLowerCase());
const account = findAccountByUsername(state, username);
accountId = account ? account.getIn(['id'], null) : -1;
}

Wyświetl plik

@ -8,7 +8,6 @@ import { expandHashtagTimeline, clearTimeline } from '../../actions/timelines';
import { FormattedMessage } from 'react-intl';
import { connectHashtagStream } from '../../actions/streaming';
import { isEqual } from 'lodash';
import ColumnBackButton from '../../components/column_back_button';
const mapStateToProps = (state, props) => ({
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
@ -26,8 +25,10 @@ class HashtagTimeline extends React.PureComponent {
};
title = () => {
const title = [this.props.params.id];
const title = [`#${this.props.params.id}`];
// TODO: wtf is all this?
// It exists in Mastodon's codebase, but undocumented
if (this.additionalFor('any')) {
title.push(' ', <FormattedMessage key='any' id='hashtag.column_header.tag_mode.any' values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />);
}
@ -43,6 +44,8 @@ class HashtagTimeline extends React.PureComponent {
return title;
}
// TODO: wtf is this?
// It exists in Mastodon's codebase, but undocumented
additionalFor = (mode) => {
const { tags } = this.props.params;
@ -108,9 +111,8 @@ class HashtagTimeline extends React.PureComponent {
const { id } = this.props.params;
return (
<Column label={`#${id}`}>
<ColumnBackButton />
<ColumnHeader icon='hashtag' active={hasUnread} title={this.title()} />
<Column label={`#${id}`} transparent>
<ColumnHeader active={hasUnread} title={this.title()} />
<StatusListContainer
scrollKey='hashtag_timeline'
timelineId={`hashtag:${id}`}

Wyświetl plik

@ -96,10 +96,11 @@ class HomeTimeline extends React.PureComponent {
render() {
const { intl, siteTitle, isLoading, isEmpty, features } = this.props;
const { done } = this.state;
const showSuggestions = features.suggestions && isEmpty && !isLoading && !done;
return (
<Column label={intl.formatMessage(messages.title)} transparent>
{(features.suggestions && isEmpty && !isLoading && !done) ? (
<Column label={intl.formatMessage(messages.title)} transparent={!showSuggestions}>
{showSuggestions ? (
<BundleContainer fetchComponent={FollowRecommendationsContainer}>
{Component => <Component onDone={this.handleDone} />}
</BundleContainer>

Wyświetl plik

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import Column from 'soapbox/features/ui/components/column';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { connectListStream } from '../../actions/streaming';
import { expandListTimeline } from '../../actions/timelines';
@ -11,9 +11,6 @@ import { fetchList, deleteList } from '../../actions/lists';
import { openModal } from '../../actions/modal';
import MissingIndicator from '../../components/missing_indicator';
import LoadingIndicator from '../../components/loading_indicator';
import Icon from 'soapbox/components/icon';
import HomeColumnHeader from '../../components/home_column_header';
import { Link } from 'react-router-dom';
import Button from 'soapbox/components/button';
const messages = defineMessages({
@ -23,7 +20,7 @@ const messages = defineMessages({
const mapStateToProps = (state, props) => ({
list: state.getIn(['lists', props.params.id]),
hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0,
// hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0,
});
export default @connect(mapStateToProps)
@ -37,7 +34,7 @@ class ListTimeline extends React.PureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
hasUnread: PropTypes.bool,
// hasUnread: PropTypes.bool,
list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
intl: PropTypes.object.isRequired,
};
@ -97,7 +94,7 @@ class ListTimeline extends React.PureComponent {
}
render() {
const { hasUnread, list } = this.props;
const { list } = this.props;
const { id } = this.props.params;
const title = list ? list.get('title') : id;
@ -126,8 +123,8 @@ class ListTimeline extends React.PureComponent {
);
return (
<Column label={title}>
<HomeColumnHeader activeItem='lists' activeSubItem={id} active={hasUnread}>
<Column label={title} heading={title} transparent>
{/* <HomeColumnHeader activeItem='lists' activeSubItem={id} active={hasUnread}>
<div className='column-header__links'>
<button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}>
<Icon id='pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
@ -144,7 +141,7 @@ class ListTimeline extends React.PureComponent {
<Icon id='arrow-right' />
</Link>
</div>
</HomeColumnHeader>
</HomeColumnHeader> */}
<StatusListContainer
scrollKey='list_timeline'

Wyświetl plik

@ -7,12 +7,16 @@ import LoadingIndicator from '../../components/loading_indicator';
import MissingIndicator from '../../components/missing_indicator';
import { fetchReblogs } from '../../actions/interactions';
import { fetchStatus } from '../../actions/statuses';
import { FormattedMessage } from 'react-intl';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import ScrollableList from '../../components/scrollable_list';
import { makeGetStatus } from '../../selectors';
const messages = defineMessages({
heading: { id: 'column.reblogs', defaultMessage: 'Reposts' },
});
const mapStateToProps = (state, props) => {
const getStatus = makeGetStatus();
const status = getStatus(state, {
@ -27,9 +31,11 @@ const mapStateToProps = (state, props) => {
};
export default @connect(mapStateToProps)
@injectIntl
class Reblogs extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.orderedSet,
@ -50,7 +56,7 @@ class Reblogs extends ImmutablePureComponent {
}
render() {
const { accountIds, status } = this.props;
const { intl, accountIds, status } = this.props;
if (!accountIds) {
return (
@ -71,7 +77,7 @@ class Reblogs extends ImmutablePureComponent {
const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has reposted this post yet. When someone does, they will show up here.' />;
return (
<Column>
<Column heading={intl.formatMessage(messages.heading)}>
<ScrollableList
scrollKey='reblogs'
emptyMessage={emptyMessage}

Wyświetl plik

@ -3,8 +3,7 @@ import { connect } from 'react-redux';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import HomeColumnHeader from '../../components/home_column_header';
import Column from 'soapbox/features/ui/components/column';
import PinnedHostsPicker from './components/pinned_hosts_picker';
import IconButton from 'soapbox/components/icon_button';
import { expandRemoteTimeline } from '../../actions/timelines';
@ -82,11 +81,10 @@ class RemoteTimeline extends React.PureComponent {
}
render() {
const { intl, hasUnread, onlyMedia, timelineId, instance, pinned } = this.props;
const { intl, onlyMedia, timelineId, instance, pinned } = this.props;
return (
<Column label={intl.formatMessage(messages.title)} transparent>
<HomeColumnHeader activeItem='fediverse' active={hasUnread} />
<Column label={intl.formatMessage(messages.title)} heading={instance} transparent>
<PinnedHostsPicker host={instance} />
{!pinned && <div className='timeline-filter-message'>
<IconButton icon='times' onClick={this.handleCloseClick} />

Wyświetl plik

@ -10,7 +10,7 @@ import { Link, NavLink } from 'react-router-dom';
import { getDomain } from 'soapbox/utils/accounts';
import Avatar from 'soapbox/components/avatar';
import DisplayName from 'soapbox/components/display_name';
import AttachmentList from 'soapbox/components/attachment_list';
import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
import PollPreview from './poll_preview';
import ScheduledStatusActionBar from './scheduled_status_action_bar';
@ -67,7 +67,7 @@ class ScheduledStatus extends ImmutablePureComponent {
collapsable
/>
<AttachmentList
<AttachmentThumbs
compact
media={status.get('media_attachments')}
/>

Wyświetl plik

@ -12,12 +12,8 @@ const messages = defineMessages({
const Search = ({ intl }) => (
<div className='column search-page'>
<ColumnHeader icon='search' title={intl.formatMessage(messages.heading)} />
<SearchContainer />
<div className='drawer__pager'>
<div className='drawer__inner darker'>
<SearchResultsContainer />
</div>
</div>
<SearchContainer autoSubmit />
<SearchResultsContainer />
</div>
);

Wyświetl plik

@ -14,11 +14,10 @@ import { getSoapboxConfig } from 'soapbox/actions/soapbox';
const mapStateToProps = state => {
const instance = state.get('instance');
const features = getFeatures(instance);
return {
allowedEmoji: getSoapboxConfig(state).get('allowedEmoji'),
reactionList: features.exposableReactions,
features: getFeatures(instance),
};
};
@ -29,7 +28,7 @@ class StatusInteractionBar extends ImmutablePureComponent {
status: ImmutablePropTypes.map,
me: SoapboxPropTypes.me,
allowedEmoji: ImmutablePropTypes.list,
reactionList: PropTypes.bool,
features: PropTypes.object.isRequired,
}
getNormalizedReacts = () => {
@ -42,7 +41,7 @@ class StatusInteractionBar extends ImmutablePureComponent {
).reverse();
}
getRepost = () => {
getReposts = () => {
const { status } = this.props;
if (status.get('reblogs_count')) {
return (
@ -58,8 +57,39 @@ class StatusInteractionBar extends ImmutablePureComponent {
return '';
}
getFavourites = () => {
const { features, status } = this.props;
if (status.get('favourites_count')) {
const favourites = (
<>
<Icon src={require('@tabler/icons/icons/thumb-up.svg')} />
<span className='emoji-reacts__count'>
<FormattedNumber value={status.get('favourites_count')} />
</span>
</>
);
if (features.exposableReactions) {
return (
<Link to={`/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}/likes`} className='emoji-react emoji-react--favourites'>
{favourites}
</Link>
);
} else {
return (
<div className='emoji-react emoji-react--favourites'>
{favourites}
</div>
);
}
}
return '';
}
getEmojiReacts = () => {
const { status, reactionList } = this.props;
const { status, features } = this.props;
const emojiReacts = this.getNormalizedReacts();
const count = emojiReacts.reduce((acc, cur) => (
@ -81,7 +111,7 @@ class StatusInteractionBar extends ImmutablePureComponent {
</>
);
if (reactionList) {
if (features.exposableReactions) {
return <Link to={`/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}/reactions/${e.get('name')}`} className='emoji-react' key={i}>{emojiReact}</Link>;
}
@ -99,13 +129,12 @@ class StatusInteractionBar extends ImmutablePureComponent {
};
render() {
const emojiReacts = this.getEmojiReacts();
const repost = this.getRepost();
const { features } = this.props;
return (
<div className='status-interaction-bar'>
{emojiReacts}
{repost}
{features.emojiReacts ? this.getEmojiReacts() : this.getFavourites()}
{this.getReposts()}
</div>
);
}

Wyświetl plik

@ -573,7 +573,7 @@ class Status extends ImmutablePureComponent {
};
return (
<Column label={intl.formatMessage(messages.detailedStatus)} showBackBtn={false}>
<Column label={intl.formatMessage(messages.detailedStatus)} transparent>
<SubNavigation message={intl.formatMessage(messages.title)} />
{/*
Eye icon to show/hide all CWs in a thread.

Wyświetl plik

@ -2,7 +2,6 @@ import React from 'react';
import ColumnHeader from './column_header';
import PropTypes from 'prop-types';
import Column from 'soapbox/components/column';
import ColumnBackButton from '../../../components/column_back_button_slim';
import DropdownMenu from 'soapbox/containers/dropdown_menu_container';
// Yes, there are 3 types of columns at this point, but this one is better, I swear
@ -29,7 +28,6 @@ export default class BetterColumn extends React.PureComponent {
<DropdownMenu items={menu} icon='ellipsis-v' size={18} direction='right' />
</div>
)}
<ColumnBackButton />
</div>
{children}
</Column>

Wyświetl plik

@ -9,7 +9,7 @@ import RelativeTimestamp from '../../../components/relative_timestamp';
import DisplayName from '../../../components/display_name';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'soapbox/components/icon';
import AttachmentList from 'soapbox/components/attachment_list';
import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
const messages = defineMessages({
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
@ -88,7 +88,7 @@ class BoostModal extends ImmutablePureComponent {
<StatusContent status={status} />
{status.get('media_attachments').size > 0 && (
<AttachmentList
<AttachmentThumbs
compact
media={status.get('media_attachments')}
/>

Wyświetl plik

@ -1,11 +0,0 @@
import React from 'react';
const DrawerLoading = () => (
<div className='drawer'>
<div className='drawer__pager'>
<div className='drawer__inner' />
</div>
</div>
);
export default DrawerLoading;

Wyświetl plik

@ -138,8 +138,17 @@ class MediaModal extends ImmutablePureComponent {
const index = this.getIndex();
let pagination = [];
const leftNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><Icon id='chevron-left' fixedWidth /></button>;
const rightNav = media.size > 1 && <button tabIndex='0' className='media-modal__nav media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><Icon id='chevron-right' fixedWidth /></button>;
const leftNav = media.size > 1 && (
<button tabIndex='0' className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}>
<Icon src={require('@tabler/icons/icons/arrow-left.svg')} />
</button>
);
const rightNav = media.size > 1 && (
<button tabIndex='0' className='media-modal__nav media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}>
<Icon src={require('@tabler/icons/icons/arrow-right.svg')} />
</button>
);
if (media.size > 1) {
pagination = media.map((item, i) => {

Wyświetl plik

@ -56,7 +56,7 @@ import {
Following,
Reblogs,
Reactions,
// Favourites,
Favourites,
DirectTimeline,
Conversations,
HashtagTimeline,
@ -259,7 +259,7 @@ class SwitchingColumnsArea extends React.PureComponent {
<Redirect from='/canary' to='/about/canary' />
<Redirect from='/canary.txt' to='/about/canary' />
<WrappedRoute path='/tags/:id' publicRoute component={HashtagTimeline} content={children} />
<WrappedRoute path='/tags/:id' publicRoute page={DefaultPage} component={HashtagTimeline} content={children} />
<WrappedRoute path='/lists' page={DefaultPage} component={Lists} content={children} />
<WrappedRoute path='/list/:id' page={HomePage} component={ListTimeline} content={children} />
@ -288,6 +288,7 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/@:username/pins' component={PinnedStatuses} page={ProfilePage} content={children} />
<WrappedRoute path='/@:username/posts/:statusId' publicRoute exact page={StatusPage} component={Status} content={children} />
<WrappedRoute path='/@:username/posts/:statusId/reblogs' page={DefaultPage} component={Reblogs} content={children} />
<WrappedRoute path='/@:username/posts/:statusId/likes' page={DefaultPage} component={Favourites} content={children} />
<WrappedRoute path='/@:username/posts/:statusId/reactions/:reaction?' page={DefaultPage} component={Reactions} content={children} />
<Redirect from='/@:username/:statusId' to='/@:username/posts/:statusId' />

Wyświetl plik

@ -119,7 +119,7 @@
"auth.invalid_credentials": "Nieprawidłowa nazwa użytkownika lub hasło",
"auth.logged_out": "Wylogowano.",
"backups.actions.create": "Utwórz kopię zapasową",
"backups.empty_message": "Nie znaleziono kopii zapasaowych. {action}",
"backups.empty_message": "Nie znaleziono kopii zapasowych. {action}",
"backups.empty_message.action": "Chcesz utworzyć?",
"backups.pending": "Oczekująca",
"boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem",
@ -525,12 +525,18 @@
"morefollows.followers_label": "…i {count} więcej {count, plural, one {obserwujący(-a)} few {obserwujących} many {obserwujących} other {obserwujących}} na zdalnych stronach.",
"morefollows.following_label": "…i {count} więcej {count, plural, one {obserwowany(-a)} few {obserwowanych} many {obserwowanych} other {obserwowanych}} na zdalnych stronach.",
"mute_modal.hide_notifications": "Chcesz ukryć powiadomienia od tego użytkownika?",
"navigation.chats": "Czaty",
"navigation.direct_messages": "Wiadomości",
"navigation.home": "Strona główna",
"navigation.notifications": "Powiadomienia",
"navigation.search": "Szukaj",
"navigation_bar.account_aliases": "Aliasy kont",
"navigation_bar.admin_settings": "Ustawienia administracyjne",
"navigation_bar.blocks": "Zablokowani użytkownicy",
"navigation_bar.bookmarks": "Zakładki",
"navigation_bar.compose": "Utwórz nowy wpis",
"navigation_bar.domain_blocks": "Ukryte domeny",
"navigation_bar.export_data": "Eksportuj dane",
"navigation_bar.favourites": "Ulubione",
"navigation_bar.filters": "Wyciszone słowa",
"navigation_bar.follow_requests": "Prośby o śledzenie",
@ -809,10 +815,13 @@
"status.unpin": "Odepnij z profilu",
"status_list.queue_label": "Naciśnij aby zobaczyć {count} {count, plural, one {nowy wpis} few {nowe wpisy} many {nowych wpisów} other {nowe wpisy}}",
"statuses.tombstone": "Jeden lub więcej z wpisów jest już niedostępny.",
"sub_navigation.back": "Wróć",
"suggestions.dismiss": "Odrzuć sugestię",
"tabs_bar.all": "Wszystkie",
"tabs_bar.apps": "Aplikacje",
"tabs_bar.chats": "Rozmowy",
"tabs_bar.dashboard": "Panel administracyjny",
"tabs_bar.fediverse": "Fediwersum",
"tabs_bar.header": "Informacje o koncie",
"tabs_bar.home": "Strona główna",
"tabs_bar.news": "Nowości",

Wyświetl plik

@ -17,10 +17,9 @@ import LinkFooter from '../features/ui/components/link_footer';
import { getAcct } from 'soapbox/utils/accounts';
import { displayFqn } from 'soapbox/utils/state';
import { getFeatures } from 'soapbox/utils/features';
import { makeGetAccount } from '../selectors';
import { Redirect } from 'react-router-dom';
import classNames from 'classnames';
import { findAccountByUsername, makeGetAccount } from 'soapbox/selectors';
const mapStateToProps = (state, { params, withReplies = false }) => {
const username = params.username || '';
@ -34,7 +33,7 @@ const mapStateToProps = (state, { params, withReplies = false }) => {
if (accountFetchError) {
accountId = null;
} else {
account = accounts.find(acct => username.toLowerCase() === acct.getIn(['acct'], '').toLowerCase());
account = findAccountByUsername(state, username);
accountId = account ? account.getIn(['id'], null) : -1;
accountUsername = account ? account.getIn(['acct'], '') : '';
}

Wyświetl plik

@ -1,7 +1,9 @@
import React from 'react';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Sticky from 'react-stickynode';
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import PrimaryNavigation from 'soapbox/components/primary_navigation';
import {
PromoPanel,
FeaturesPanel,
@ -36,14 +38,9 @@ class RemoteInstancePage extends ImmutablePureComponent {
<div className='columns-area__panels__pane columns-area__panels__pane--left'>
<div className='columns-area__panels__pane__inner'>
<BundleContainer fetchComponent={InstanceInfoPanel}>
{Component => <Component host={host} />}
</BundleContainer>
{(disclosed || isAdmin) && (
<BundleContainer fetchComponent={InstanceModerationPanel}>
{Component => <Component host={host} />}
</BundleContainer>
)}
<Sticky top={65}>
<PrimaryNavigation />
</Sticky>
</div>
</div>
@ -55,15 +52,25 @@ class RemoteInstancePage extends ImmutablePureComponent {
<div className='columns-area__panels__pane columns-area__panels__pane--right'>
<div className='columns-area__panels__pane__inner'>
{me && (
<BundleContainer fetchComponent={FeaturesPanel}>
{Component => <Component key='features-panel' />}
<Sticky top={65}>
{me && (
<BundleContainer fetchComponent={FeaturesPanel}>
{Component => <Component key='features-panel' />}
</BundleContainer>
)}
<BundleContainer fetchComponent={PromoPanel}>
{Component => <Component key='promo-panel' />}
</BundleContainer>
)}
<BundleContainer fetchComponent={PromoPanel}>
{Component => <Component key='promo-panel' />}
</BundleContainer>
<LinkFooter key='link-footer' />
<BundleContainer fetchComponent={InstanceInfoPanel}>
{Component => <Component host={host} />}
</BundleContainer>
{(disclosed || isAdmin) && (
<BundleContainer fetchComponent={InstanceModerationPanel}>
{Component => <Component host={host} />}
</BundleContainer>
)}
<LinkFooter key='link-footer' />
</Sticky>
</div>
</div>
</div>

Wyświetl plik

@ -47,6 +47,36 @@ export const makeGetAccount = () => {
});
};
const findAccountsByUsername = (state, username) => {
const accounts = state.get('accounts');
return accounts.filter(account => {
return username.toLowerCase() === account.getIn(['acct'], '').toLowerCase();
});
};
export const findAccountByUsername = (state, username) => {
const accounts = findAccountsByUsername(state, username);
if (accounts.size > 1) {
const me = state.get('me');
const meURL = state.getIn(['accounts', me, 'url']);
return accounts.find(account => {
try {
// If more than one account has the same username, try matching its host
const { host } = new URL(account.get('url'));
const { host: meHost } = new URL(meURL);
return host === meHost;
} catch {
return false;
}
});
} else {
return accounts.first();
}
};
const toServerSideType = columnType => {
switch (columnType) {
case 'home':

Wyświetl plik

@ -3,7 +3,9 @@
display: block;
text-decoration: none;
color: inherit;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.3);
border-radius: 4px;
overflow: hidden;
@media screen and (max-width: $no-gap-breakpoint) {
box-shadow: none;
@ -13,7 +15,7 @@
&:active,
&:focus {
.card__bar {
background: var(--foreground-color);
background-color: var(--brand-color--faint);
}
}
}
@ -22,7 +24,6 @@
height: 130px;
position: relative;
background: var(--background-color);
border-radius: 4px 4px 0 0;
.still-image {
display: block;
@ -30,7 +31,6 @@
height: 100%;
margin: 0;
object-fit: cover;
border-radius: 4px 4px 0 0;
}
@media screen and (max-width: 600px) {
@ -48,12 +48,8 @@
display: flex;
justify-content: flex-start;
align-items: center;
background: var(--brand-color--faint);
border-radius: 0 0 4px 4px;
@media screen and (max-width: $no-gap-breakpoint) {
border-radius: 0;
}
background: var(--background-color);
transition: 0.2s;
.avatar {
flex: 0 0 auto;

Wyświetl plik

@ -58,7 +58,6 @@
@import 'components/react-toggle';
@import 'components/getting-started';
@import 'components/promo-panel';
@import 'components/drawer';
@import 'components/still-image';
@import 'components/timeline-queue-header';
@import 'components/badge';

Wyświetl plik

@ -296,20 +296,22 @@
position: relative;
.icon-button {
color: var(--primary-text-color--faint);
color: var(--primary-text-color);
position: absolute;
right: 10px;
top: calc(50% - 13px);
top: 50%;
transform: translateY(-50%);
width: auto;
height: auto;
background: transparent !important;
border: 0;
padding: 0;
margin: 0;
}
.chat-box__send .icon-button {
top: calc(50% - 9px);
.svg-icon {
width: 18px;
height: 18px;
}
}
textarea {
@ -332,7 +334,7 @@
.ui--chatroom {
padding-bottom: 0;
.columns-area__panels__main .columns-area {
.columns-area__panels__main .columns-area .column {
height: calc(100vh - 100px);
box-sizing: border-box;
overflow: hidden;

Wyświetl plik

@ -313,9 +313,11 @@
display: flex;
font-size: 14px;
color: var(--primary-text-color--faint);
@media screen and (max-width: 895px) {
justify-content: center;
flex-wrap: wrap;
display: none;
}
a {

Wyświetl plik

@ -61,6 +61,7 @@
box-sizing: border-box;
display: flex;
flex-direction: column;
flex: 1 1 100%;
}
@media screen and (min-width: 631px) {
@ -68,8 +69,7 @@
padding: 0;
}
.column,
.drawer {
.column {
flex: 0 0 auto;
padding: 10px;
padding-left: 5px;
@ -85,8 +85,7 @@
}
.columns-area > div {
.column,
.drawer {
.column {
padding-left: 5px;
padding-right: 5px;
}
@ -98,12 +97,10 @@
flex-direction: column;
width: 100%;
margin: 0 auto;
padding: 15px 0;
padding-top: 15px;
.column,
.drawer {
.column {
width: 100%;
height: 100%;
padding: 0;
}
@ -125,11 +122,9 @@
}
@media (max-width: 580px) {
padding: 0;
.timeline-compose-block {
border-radius: 0;
margin-top: 10px; // Make less claustrophobic
margin-top: -5px;
}
}
@ -352,6 +347,10 @@
background: transparent;
border-radius: 0;
box-shadow: none;
.sub-navigation {
box-shadow: 0 -6px 6px -6px rgba(0, 0, 0, 0.1);
}
}
@media screen and (max-width: 580px) {
@ -710,6 +709,7 @@
align-items: center;
justify-content: center;
min-height: 160px;
border-radius: 0 0 10px 10px;
@supports (display: grid) { // hack to fix Chrome <57
contain: strict;
@ -727,21 +727,20 @@
text-decoration: underline;
}
}
@media screen and (max-width: 580px) {
border-radius: 0;
}
}
.error-column {
flex-direction: column;
border-radius: 0 0 10px 10px;
.svg-icon {
width: 70px;
height: 70px;
margin-bottom: 30px;
}
@media screen and (max-width: 580px) {
border-radius: 0;
}
}
.column-link--transparent .icon-with-badge__badge {
@ -775,6 +774,11 @@
.column__top {
display: flex;
align-items: center;
border-bottom: 1px solid hsla(var(--primary-text-color_hsl), 0.2);
.sub-navigation {
border-bottom: 0;
}
}
.column-header {
@ -856,6 +860,10 @@
.column-list {
position: relative;
&__empty-message {
padding: 0 20px;
}
}
.column-loading {
@ -882,6 +890,10 @@
.column--transparent {
.slist__append {
@include standard-panel;
@media screen and (max-width: 580px) {
border-radius: 0;
}
}
.sub-navigation ~ .slist .slist__append {

Wyświetl plik

@ -27,7 +27,7 @@
}
}
.compose-form__warning {
&__warning {
color: var(--primary-text-color);
margin-bottom: 10px;
background: var(--brand-color--faint);
@ -66,7 +66,7 @@
right: 5px;
}
.compose-form__autosuggest-wrapper {
&__autosuggest-wrapper {
position: relative;
}
@ -195,7 +195,7 @@
color: var(--primary-text-color--faint);
}
.compose-form__modifiers {
&__modifiers {
color: var(--primary-text-color);
font-family: inherit;
font-size: 14px;
@ -327,7 +327,7 @@
}
} // end .compose-form .compose-form__modifiers
.compose-form__buttons-wrapper {
&__buttons-wrapper {
padding: 10px;
background: var(--background-color);
display: flex;
@ -380,13 +380,23 @@
}
}
.character-counter__wrapper {
align-self: center;
margin: 0 10px 0 auto;
.character-counter {
display: block;
cursor: default;
font-family: var(--font-sans-serif), sans-serif;
font-size: 14px;
font-weight: 600;
color: var(--primary-text-color--faint);
&.character-counter--over { color: $warning-red; }
}
.character-counter,
.visual-character-counter {
margin-right: 10px;
}
}
.compose-form__publish {
&__publish {
display: flex;
justify-content: flex-end;
min-width: 0;
@ -396,6 +406,13 @@
overflow: hidden;
}
}
&__counter {
display: flex;
align-items: center;
align-self: center;
margin-left: auto;
}
} // end .compose-form
// Icon tweaks

Wyświetl plik

@ -71,7 +71,6 @@
.detailed-status__link {
color: var(--primary-text-color--faint);
cursor: pointer;
text-decoration: none;
font-size: 13px;
}
@ -159,8 +158,34 @@
}
.thread {
@include standard-panel;
border-top-left-radius: 0;
border-top-right-radius: 0;
@media screen and (max-width: 580px) {
border-radius: 0;
}
&__status {
position: relative;
// Only display line if posts are below
&:last-child .detailed-status__action-bar {
border-bottom: 0;
}
}
&__descendants .thread__status:first-child {
margin-top: 10px;
}
&__status--focused:first-child,
&__ancestors &__status:first-child {
margin-top: 10px;
}
&__descendants &__status:last-child {
margin-bottom: 10px;
}
&__connector {

Wyświetl plik

@ -1,80 +0,0 @@
.drawer {
width: 300px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow-y: hidden;
}
.drawer__tab {
display: block;
flex: 1 1 auto;
padding: 15px 5px 13px;
color: var(--primary-text-color--faint);
text-decoration: none;
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
}
.column,
.drawer {
flex: 1 1 100%;
}
.drawer__pager {
box-sizing: border-box;
padding: 0;
flex-grow: 1;
position: relative;
overflow: hidden;
display: flex;
}
.drawer__inner {
top: 0;
left: 0;
background: var(--foreground-color);
box-sizing: border-box;
padding: 0;
display: flex;
flex-direction: column;
overflow: hidden;
overflow-y: auto;
width: 100%;
height: 100%;
}
.pseudo-drawer {
background: var(--background-color);
font-size: 13px;
text-align: left;
}
.drawer__header {
flex: 0 0 auto;
font-size: 16px;
background: var(--brand-color--med);
margin-bottom: 10px;
display: flex;
flex-direction: row;
a {
transition: background 100ms ease-in;
&:hover {
background: var(--background-color);
transition: background 200ms ease-out;
}
}
}
.drawer__backdrop {
cursor: pointer;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba($base-overlay-background, 0.5);
}

Wyświetl plik

@ -21,12 +21,19 @@
}
}
.emoji-react--reblogs {
.emoji-react--reblogs,
.emoji-react--favourites {
vertical-align: middle;
display: inline-flex;
margin-right: 10px;
.svg-icon {
margin-right: 0.2em;
}
}
.emoji-react--reblogs {
.svg-icon {
color: var(--highlight-text-color);
svg {
@ -35,6 +42,12 @@
}
}
.emoji-react--favourites {
.svg-icon {
color: $gold-star;
}
}
.emoji-reacts {
display: inline-flex;
flex-direction: row-reverse;

Wyświetl plik

@ -17,8 +17,7 @@
&__footer {
flex: 0 0 auto;
padding: 10px;
padding-top: 20px;
padding: 20px 10px 40px;
ul {
margin-bottom: 10px;

Wyświetl plik

@ -14,16 +14,6 @@
border-radius: 8px 8px 0 0;
}
.drawer__inner {
border-radius: 0 0 8px 8px;
&.backdrop {
width: calc(100% - 60px);
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
border-radius: 0 0 0 8px;
}
}
&__accounts {
background: var(--background-color);
overflow-y: auto;

Wyświetl plik

@ -192,3 +192,33 @@
}
}
}
$media-compact-size: 50px;
.media-gallery--compact {
height: $media-compact-size !important;
background: transparent;
.spoiler-button {
display: none;
}
.media-gallery__item {
width: $media-compact-size !important;
height: $media-compact-size !important;
inset: auto !important;
margin-right: 5px;
&-overflow {
font-size: 20px;
}
&__icons {
font-size: 30px;
}
}
.media-gallery__file-extension__label {
display: none;
}
}

Wyświetl plik

@ -128,8 +128,9 @@
@media screen and (max-width: 600px) { padding: 30px 2px; }
.fa {
margin-right: 0;
.svg-icon {
width: 24px;
height: 24px;
}
}
@ -180,7 +181,7 @@
}
.media-modal__button {
background-color: var(--primary-text-color);
background-color: #fff;
height: 12px;
width: 12px;
border-radius: 6px;
@ -337,7 +338,7 @@
overflow: hidden;
width: 480px;
max-width: 90vw;
border-radius: 4px;
border-radius: 10px;
border: 1px solid var(--primary-text-color--faint);
color: var(--primary-text-color--faint);
background: var(--foreground-color);

Wyświetl plik

@ -29,9 +29,11 @@
}
.pinned-hosts-picker {
margin-left: 10px;
padding: 10px 0 0 10px;
display: inline-flex;
flex-wrap: wrap;
background: var(--foreground-color);
border-bottom: 1px solid hsla(var(--primary-text-color_hsl), 0.2);
.pinned-host {
margin-right: 10px;

Wyświetl plik

@ -166,9 +166,7 @@
}
.search-page {
.drawer__inner:not(:empty) {
min-height: 48px;
}
height: 100%;
.search {
padding: 10px 15px;
@ -184,14 +182,6 @@
.search__icon .svg-icon {
right: 24px;
}
.drawer__pager {
border-radius: 0 0 10px 10px;
@media screen and (max-width: 450px) {
border-radius: 0;
}
}
}
.search-results {

Wyświetl plik

@ -143,6 +143,10 @@
.status__prepend-icon-wrapper {
left: -26px;
position: absolute;
svg.feather-repeat {
color: var(--highlight-text-color);
}
}
.status {
@ -710,3 +714,18 @@ a.status-card.compact:hover {
padding: 15px 0 10px;
}
}
.attachment-thumbs {
position: relative;
&__clickable-region {
cursor: pointer;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}

Wyświetl plik

@ -1,35 +1,35 @@
.timeline-queue-header {
display: flex;
align-self: center;
align-items: center;
justify-content: space-evenly;
max-height: 30px;
position: sticky;
height: 30px;
position: fixed;
top: 60px;
margin: 0 auto;
margin-bottom: 8px;
background-color: var(--brand-color);
color: #fff;
border-bottom: 1px solid;
border-top: 1px solid;
border-color: var(--brand-color--faint);
border-radius: 100px;
transition: max-height 150ms ease;
transition: 150ms ease;
overflow: hidden;
opacity: 1;
left: 0;
right: 0;
padding: 0 10px;
z-index: 500;
.sub-navigation ~ & {
top: calc(60px + 41px);
}
.svg-icon {
margin-right: 5px;
}
&.hidden {
max-height: 0;
opacity: 0;
margin: 0;
border: 0;
transform: scaleY(0);
pointer-events: none;
.timeline-queue-header__label {
opacity: 0;
}
}
&__btn {
@ -46,4 +46,8 @@
height: 46px;
}
}
&__label {
transition: 150ms ease;
}
}

Wyświetl plik

@ -297,7 +297,7 @@
left: 0;
top: 50%;
transform: translate(0, -50%);
background: var(--brand-color);
background: var(--accent-color);
}
&__handle {
@ -310,7 +310,7 @@
left: 0;
margin-left: -6px;
transform: translate(0, -50%);
background: var(--brand-color);
background: var(--accent-color);
box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
opacity: 0;
@ -364,7 +364,7 @@
height: 4px;
border-radius: 4px;
top: 14px;
background: var(--brand-color);
background: var(--accent-color);
}
&__buffer {
@ -380,7 +380,7 @@
height: 12px;
top: 10px;
margin-left: -6px;
background: var(--brand-color);
background: var(--accent-color);
box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
.no-reduce-motion & {

Wyświetl plik

@ -113,6 +113,10 @@
justify-content: center;
z-index: 999;
&--scrolled {
border-radius: 0 !important;
}
&__content {
width: 100%;
height: 100%;

Wyświetl plik

@ -228,8 +228,7 @@ body.rtl {
}
@media screen and (min-width: 631px) {
.column,
.drawer {
.column {
padding-left: 5px;
padding-right: 5px;
@ -240,8 +239,7 @@ body.rtl {
}
.columns-area > div {
.column,
.drawer {
.column {
padding-left: 5px;
padding-right: 5px;
}

Wyświetl plik

@ -318,7 +318,6 @@
.react-swipeable-view-container {
&,
.columns-area,
.drawer,
.column {
height: 100%;
}

Wyświetl plik

@ -116,6 +116,7 @@
"qrcode.react": "^1.0.0",
"react": "^16.13.1",
"react-color": "^2.18.1",
"react-content-loader": "^6.0.3",
"react-datepicker": "^4.1.1",
"react-dom": "^16.13.1",
"react-helmet": "^6.0.0",

Wyświetl plik

@ -7757,6 +7757,11 @@ react-color@^2.18.1:
reactcss "^1.2.0"
tinycolor2 "^1.4.1"
react-content-loader@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/react-content-loader/-/react-content-loader-6.0.3.tgz#32e28ca7120e0a2552fc26655d0d4448cc1fc0c5"
integrity sha512-CIRgTHze+ls+jGDIfCitw27YkW2XcaMpsYORTUdBxsMFiKuUYMnlvY76dZE4Lsaa9vFXVw+41ieBEK7SJt0nug==
react-datepicker@^4.1.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.2.1.tgz#72caf5055bc7c4eb0279c1f6d7624ded053edc4c"