Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Essem 2023-12-10 23:28:41 -06:00
commit ce1c652623
No known key found for this signature in database
GPG key ID: 7D497397CC3A2A8C
55 changed files with 292 additions and 304 deletions

View file

@ -154,6 +154,7 @@ GEM
net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8)
base64 (0.1.1)
bcp47_spec (0.2.1)
bcrypt (3.1.19)
better_errors (2.10.1)
erubi (>= 1.0.0)
@ -375,19 +376,19 @@ GEM
reline (>= 0.3.8)
jmespath (1.6.2)
json (2.6.3)
json-canonicalization (0.3.2)
json-canonicalization (1.0.0)
json-jwt (1.15.3)
activesupport (>= 4.2)
aes_key_wrap
bindata
httpclient
json-ld (3.2.5)
json-ld (3.3.1)
htmlentities (~> 4.3)
json-canonicalization (~> 0.3, >= 0.3.2)
json-canonicalization (~> 1.0)
link_header (~> 0.0, >= 0.0.8)
multi_json (~> 1.15)
rack (>= 2.2, < 4)
rdf (~> 3.2, >= 3.2.10)
rdf (~> 3.3)
json-ld-preloaded (3.2.2)
json-ld (~> 3.2)
rdf (~> 3.2)
@ -596,7 +597,8 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.0.6)
rdf (3.2.11)
rdf (3.3.1)
bcp47_spec (~> 0.2)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.6.1)
rdf (~> 3.2)

View file

@ -17,7 +17,6 @@ import { Avatar } from './avatar';
import { Button } from './button';
import { FollowersCounter } from './counters';
import { DisplayName } from './display_name';
import { IconButton } from './icon_button';
import Permalink from './permalink';
import { RelativeTimestamp } from './relative_timestamp';
@ -45,10 +44,7 @@ class Account extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
hidden: PropTypes.bool,
minimal: PropTypes.bool,
actionIcon: PropTypes.string,
actionTitle: PropTypes.string,
defaultAction: PropTypes.string,
onActionClick: PropTypes.func,
withBio: PropTypes.bool,
};
@ -76,12 +72,8 @@ class Account extends ImmutablePureComponent {
this.props.onMuteNotifications(this.props.account, false);
};
handleAction = () => {
this.props.onActionClick(this.props.account);
};
render () {
const { account, intl, hidden, withBio, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal } = this.props;
const { account, intl, hidden, withBio, defaultAction, size, minimal } = this.props;
if (!account) {
return <EmptyAccount size={size} minimal={minimal} />;
@ -98,9 +90,7 @@ class Account extends ImmutablePureComponent {
let buttons;
if (actionIcon && onActionClick) {
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
} else if (!actionIcon && account.get('id') !== me && account.get('relationship', null) !== null) {
if (account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);

View file

@ -5,49 +5,44 @@ import { autoPlayGif } from '../initial_state';
import type { Account } from '../types/resources';
interface Props {
account: Account | undefined;
className?: string;
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
size: number;
style?: React.CSSProperties;
inline?: boolean;
animate?: boolean;
}
export const Avatar: React.FC<Props> = ({
account,
className,
animate = autoPlayGif,
size = 20,
inline = false,
style: styleFromParent,
}) => {
const { hovering, handleMouseEnter, handleMouseLeave } =
useHovering(autoPlayGif);
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
const style = {
...styleFromParent,
width: `${size}px`,
height: `${size}px`,
backgroundSize: `${size}px ${size}px`,
};
if (account) {
style.backgroundImage = `url(${account.get(
hovering ? 'avatar' : 'avatar_static',
)})`;
}
const src =
hovering || animate
? account?.get('avatar')
: account?.get('avatar_static');
return (
<div
className={classNames(
'account__avatar',
{ 'account__avatar-inline': inline },
className,
)}
className={classNames('account__avatar', {
'account__avatar-inline': inline,
})}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={style}
data-avatar-of={account && `@${account.get('acct')}`}
role='img'
aria-label={account?.get('acct')}
/>
>
{src && <img src={src} alt={account?.get('acct')} />}
</div>
);
};

View file

@ -5,6 +5,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from '../initial_state';
import { Avatar } from './avatar';
export default class AvatarComposite extends PureComponent {
static propTypes = {
@ -76,12 +78,12 @@ export default class AvatarComposite extends PureComponent {
bottom: bottom,
width: `${width}%`,
height: `${height}%`,
backgroundSize: 'cover',
backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
};
return (
<div key={account.get('id')} style={style} data-avatar-of={`@${account.get('acct')}`} />
<div key={account.get('id')} style={style}>
<Avatar account={account} animate={animate} />
</div>
);
}

View file

@ -1,39 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from 'flavours/glitch/initial_state';
export default class AvatarOverlay extends PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
friend: ImmutablePropTypes.map.isRequired,
animate: PropTypes.bool,
};
static defaultProps = {
animate: autoPlayGif,
};
render() {
const { account, friend, animate } = this.props;
const baseStyle = {
backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
};
const overlayStyle = {
backgroundImage: `url(${friend.get(animate ? 'avatar' : 'avatar_static')})`,
};
return (
<div className='account__avatar-overlay'>
<div className='account__avatar-overlay-base' style={baseStyle} data-avatar-of={`@${account.get('acct')}`} />
<div className='account__avatar-overlay-overlay' style={overlayStyle} data-avatar-of={`@${friend.get('acct')}`} />
</div>
);
}
}

View file

@ -0,0 +1,56 @@
import { useHovering } from '../hooks/useHovering';
import { autoPlayGif } from '../initial_state';
import type { Account } from '../types/resources';
interface Props {
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
size?: number;
baseSize?: number;
overlaySize?: number;
}
export const AvatarOverlay: React.FC<Props> = ({
account,
friend,
size = 46,
baseSize = 36,
overlaySize = 24,
}) => {
const { hovering, handleMouseEnter, handleMouseLeave } =
useHovering(autoPlayGif);
const accountSrc = hovering
? account?.get('avatar')
: account?.get('avatar_static');
const friendSrc = hovering
? friend?.get('avatar')
: friend?.get('avatar_static');
return (
<div
className='account__avatar-overlay'
style={{ width: size, height: size }}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className='account__avatar-overlay-base'>
<div
className='account__avatar'
style={{ width: `${baseSize}px`, height: `${baseSize}px` }}
data-avatar-of={`@${account?.get('acct')}`}
>
{accountSrc && <img src={accountSrc} alt={account?.get('acct')} />}
</div>
</div>
<div className='account__avatar-overlay-overlay'>
<div
className='account__avatar'
style={{ width: `${overlaySize}px`, height: `${overlaySize}px` }}
data-avatar-of={`@${friend?.get('acct')}`}
>
{friendSrc && <img src={friendSrc} alt={friend?.get('acct')} />}
</div>
</div>
</div>
);
};

View file

@ -12,7 +12,6 @@ export default class Column extends PureComponent {
static propTypes = {
children: PropTypes.node,
extraClasses: PropTypes.string,
name: PropTypes.string,
label: PropTypes.string,
bindToDocument: PropTypes.bool,
};
@ -62,10 +61,10 @@ export default class Column extends PureComponent {
}
render () {
const { children, extraClasses, name, label } = this.props;
const { label, children, extraClasses } = this.props;
return (
<div role='region' aria-label={label} data-column={name} className={`column ${extraClasses || ''}`} ref={this.setRef}>
<div role='region' aria-label={label} className={`column ${extraClasses || ''}`} ref={this.setRef}>
{children}
</div>
);

View file

@ -84,6 +84,7 @@ class Status extends ImmutablePureComponent {
previousId: PropTypes.string,
nextInReplyToId: PropTypes.string,
rootId: PropTypes.string,
onClick: PropTypes.func,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
@ -116,7 +117,6 @@ class Status extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
onClick: PropTypes.func,
scrollKey: PropTypes.string,
deployPictureInPicture: PropTypes.func,
settings: ImmutablePropTypes.map.isRequired,
@ -576,7 +576,7 @@ class Status extends ImmutablePureComponent {
openProfile: this.handleHotkeyOpenProfile,
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
toggleSpoiler: this.handleExpandedToggle,
toggleHidden: this.handleExpandedToggle,
bookmark: this.handleHotkeyBookmark,
toggleCollapse: this.handleHotkeyCollapse,
toggleSensitive: this.handleHotkeyToggleSensitive,

View file

@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
// Mastodon imports.
import { Avatar } from './avatar';
import AvatarOverlay from './avatar_overlay';
import { AvatarOverlay } from './avatar_overlay';
import { DisplayName } from './display_name';
export default class StatusHeader extends PureComponent {
@ -39,7 +39,7 @@ export default class StatusHeader extends PureComponent {
let statusAvatar;
if (friend === undefined || friend === null) {
statusAvatar = <Avatar account={account} size={48} />;
statusAvatar = <Avatar account={account} size={46} />;
} else {
statusAvatar = <AvatarOverlay account={account} friend={friend} />;
}

View file

@ -22,6 +22,7 @@ import { store } from 'flavours/glitch/store';
const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
const hydrateAction = hydrateStore(initialState);
store.dispatch(hydrateAction);
// check for deprecated local settings
@ -71,8 +72,8 @@ export default class Mastodon extends PureComponent {
}
}
shouldUpdateScroll (_, { location }) {
return !(location.state?.mastodonModalKey);
shouldUpdateScroll (prevRouterProps, { location }) {
return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey);
}
render () {

View file

@ -38,7 +38,7 @@ class FeaturedTags extends ImmutablePureComponent {
name={featuredTag.get('name')}
href={featuredTag.get('url')}
to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`}
uses={featuredTag.get('statuses_count')}
uses={featuredTag.get('statuses_count') * 1}
withGraph={false}
description={((featuredTag.get('statuses_count') * 1) > 0) ? intl.formatMessage(messages.lastStatusAt, { date: intl.formatDate(featuredTag.get('last_status_at'), { month: 'short', day: '2-digit' }) }) : intl.formatMessage(messages.empty)}
/>

View file

@ -11,7 +11,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { Avatar } from 'flavours/glitch/components/avatar';
import { Button } from 'flavours/glitch/components/button';
import { Icon } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state';

View file

@ -76,7 +76,7 @@ export default class MediaItem extends ImmutablePureComponent {
if (['audio', 'video'].includes(attachment.get('type'))) {
content = (
<img
src={attachment.get('preview_url') || attachment.getIn(['account', 'avatar_static'])}
src={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
alt={attachment.get('description')}
lang={status.get('language')}
onLoad={this.handleImageLoad}

View file

@ -8,7 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { Icon } from 'flavours/glitch/components/icon';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import AvatarOverlay from '../../../components/avatar_overlay';
import { AvatarOverlay } from '../../../components/avatar_overlay';
import { DisplayName } from '../../../components/display_name';
class MovedNote extends ImmutablePureComponent {

View file

@ -184,7 +184,7 @@ class AccountTimeline extends ImmutablePureComponent {
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
return (
<Column ref={this.setRef} name='account'>
<Column ref={this.setRef}>
<ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
<StatusList

View file

@ -9,7 +9,7 @@ import { is } from 'immutable';
import { throttle, debounce } from 'lodash';
import { Icon } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { formatTime, getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video';
import { Blurhash } from '../../components/blurhash';

View file

@ -59,7 +59,7 @@ class Blocks extends ImmutablePureComponent {
const emptyMessage = <FormattedMessage id='empty_column.blocks' defaultMessage="You haven't blocked any users yet." />;
return (
<Column name='blocks' bindToDocument={!multiColumn} icon='ban' heading={intl.formatMessage(messages.heading)}>
<Column bindToDocument={!multiColumn} icon='ban' heading={intl.formatMessage(messages.heading)}>
<ColumnBackButtonSlim />
<ScrollableList
scrollKey='blocks'

View file

@ -77,7 +77,7 @@ class Bookmarks extends ImmutablePureComponent {
const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses' defaultMessage="You don't have any bookmarked posts yet. When you bookmark one, it will show up here." />;
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} name='bookmarks'>
<Column bindToDocument={!multiColumn} ref={this.setRef}>
<ColumnHeader
icon='bookmark'
title={intl.formatMessage(messages.heading)}

View file

@ -40,14 +40,14 @@ const mapStateToProps = (state, { columnId }) => {
class CommunityTimeline extends PureComponent {
static defaultProps = {
onlyMedia: false,
};
static contextTypes = {
identity: PropTypes.object,
};
static defaultProps = {
onlyMedia: false,
};
static propTypes = {
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
@ -128,7 +128,7 @@ class CommunityTimeline extends PureComponent {
const pinned = !!columnId;
return (
<Column ref={this.setRef} name='local' bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='users'
active={hasUnread}

View file

@ -17,19 +17,21 @@ export default class NavigationBar extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
onLogout: PropTypes.func.isRequired,
onClose: PropTypes.func,
};
render () {
const username = this.props.account.get('acct');
return (
<div className='navigation-bar'>
<Permalink className='avatar' href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}>
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
<Avatar account={this.props.account} size={48} />
<Permalink className='avatar' href={this.props.account.get('url')} to={`/@${username}`}>
<span style={{ display: 'none' }}>{username}</span>
<Avatar account={this.props.account} size={46} />
</Permalink>
<div className='navigation-bar__profile'>
<Permalink className='acct' href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}>
<strong>@{this.props.account.get('acct')}</strong>
<strong className='navigation-bar__profile-account'>@{username}</strong>
</Permalink>
{ profileLink !== undefined && (

View file

@ -92,25 +92,6 @@ class Search extends PureComponent {
}
};
handleBlur = () => {
this.setState({ expanded: false, selectedOption: -1 });
};
handleFocus = () => {
const { onShow, singleColumn } = this.props;
this.setState({ expanded: true, selectedOption: -1 });
onShow();
if (this.searchForm && !singleColumn) {
const { left, right } = this.searchForm.getBoundingClientRect();
if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) {
this.searchForm.scrollIntoView();
}
}
};
handleKeyDown = (e) => {
const { selectedOption } = this.state;
const options = searchEnabled ? this._getOptions().concat(this.defaultOptions) : this._getOptions();
@ -161,8 +142,23 @@ class Search extends PureComponent {
}
};
findTarget = () => {
return this.searchForm;
handleFocus = () => {
const { onShow, singleColumn } = this.props;
this.setState({ expanded: true, selectedOption: -1 });
onShow();
if (this.searchForm && !singleColumn) {
const { left, right } = this.searchForm.getBoundingClientRect();
if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) {
this.searchForm.scrollIntoView();
}
}
};
handleBlur = () => {
this.setState({ expanded: false, selectedOption: -1 });
};
handleHashtagClick = () => {

View file

@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { Icon } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { LoadMore } from 'flavours/glitch/components/load_more';
import { SearchSection } from 'flavours/glitch/features/explore/components/search_section';
@ -69,6 +69,7 @@ class SearchResults extends ImmutablePureComponent {
);
}
return (
<div className='drawer--results'>
<header className='search-results__header'>

View file

@ -77,7 +77,7 @@ class Favourites extends ImmutablePureComponent {
const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favorite posts yet. When you favorite one, it will show up here." />;
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} name='favourites' label={intl.formatMessage(messages.heading)}>
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
<ColumnHeader
icon='star'
title={intl.formatMessage(messages.heading)}

View file

@ -85,9 +85,10 @@ class Favourites extends ImmutablePureComponent {
showBackButton
multiColumn={multiColumn}
extraButton={(
<button className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button>
<button type='button' className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' /></button>
)}
/>
<ScrollableList
scrollKey='favourites'
onLoadMore={this.handleLoadMore}

View file

@ -67,7 +67,7 @@ class FollowRequests extends ImmutablePureComponent {
);
return (
<Column bindToDocument={!multiColumn} name='follow-requests' icon='user-plus' heading={intl.formatMessage(messages.heading)}>
<Column bindToDocument={!multiColumn} icon='user-plus' heading={intl.formatMessage(messages.heading)}>
<ColumnBackButtonSlim />
<ScrollableList
scrollKey='follow_requests'

View file

@ -394,7 +394,7 @@ class Announcements extends ImmutablePureComponent {
_markAnnouncementAsRead () {
const { dismissAnnouncement, announcements } = this.props;
const { index } = this.state;
const announcement = announcements.get(index);
const announcement = announcements.get(announcements.size - 1 - index);
if (!announcement.get('read')) dismissAnnouncement(announcement.get('id'));
}
@ -435,7 +435,7 @@ class Announcements extends ImmutablePureComponent {
selected={index === idx}
disabled={disableSwiping}
/>
))}
)).reverse()}
</ReactSwipeableViews>
{announcements.size > 1 && (

View file

@ -173,7 +173,7 @@ class GettingStarted extends ImmutablePureComponent {
}
return (
<Column bindToDocument={!multiColumn} name='getting-started' icon='asterisk' heading={intl.formatMessage(messages.heading)} label={intl.formatMessage(messages.menu)} hideHeadingOnMobile>
<Column bindToDocument={!multiColumn} icon='asterisk' heading={intl.formatMessage(messages.heading)} label={intl.formatMessage(messages.menu)} hideHeadingOnMobile>
<div className='scrollable optionally-scrollable'>
<div className='getting-started__wrapper'>
{!multiColumn && signedIn && <NavigationBar account={myAccount} />}

View file

@ -196,7 +196,7 @@ class HomeTimeline extends PureComponent {
}
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} name='home' label={intl.formatMessage(messages.title)}>
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='home'
active={hasUnread}

View file

@ -16,7 +16,7 @@ const messages = defineMessages({
add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
});
const mapStateToProps = (state, { listId, added }) => ({
const MapStateToProps = (state, { listId, added }) => ({
list: state.get('lists').get(listId),
added: typeof added === 'undefined' ? state.getIn(['listAdder', 'lists', 'items']).includes(listId) : added,
});
@ -69,4 +69,4 @@ class List extends ImmutablePureComponent {
}
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(List));
export default connect(MapStateToProps, mapDispatchToProps)(injectIntl(List));

View file

@ -1,20 +1,39 @@
import PropTypes from 'prop-types';
import { defineMessages } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { removeFromListEditor, addToListEditor } from '../../../actions/lists';
import { Avatar } from '../../../components/avatar';
import { DisplayName } from '../../../components/display_name';
import { IconButton } from '../../../components/icon_button';
import { makeGetAccount } from '../../../selectors';
const messages = defineMessages({
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
});
export default class Account extends ImmutablePureComponent {
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, { accountId, added }) => ({
account: getAccount(state, accountId),
added: typeof added === 'undefined' ? state.getIn(['listEditor', 'accounts', 'items']).includes(accountId) : added,
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { accountId }) => ({
onRemove: () => dispatch(removeFromListEditor(accountId)),
onAdd: () => dispatch(addToListEditor(accountId)),
});
class Account extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
@ -56,3 +75,5 @@ export default class Account extends ImmutablePureComponent {
}
}
export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(Account));

View file

@ -1,17 +1,31 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { Icon } from 'flavours/glitch/components/icon';
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
const messages = defineMessages({
search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
});
export default class Search extends PureComponent {
const mapStateToProps = state => ({
value: state.getIn(['listEditor', 'suggestions', 'value']),
});
const mapDispatchToProps = dispatch => ({
onSubmit: value => dispatch(fetchListSuggestions(value)),
onClear: () => dispatch(clearListSuggestions()),
onChange: value => dispatch(changeListSuggestions(value)),
});
class Search extends PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
@ -63,3 +77,5 @@ export default class Search extends PureComponent {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(Search));

View file

@ -1,26 +0,0 @@
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { removeFromListEditor, addToListEditor } from 'flavours/glitch/actions/lists';
import { makeGetAccount } from 'flavours/glitch/selectors';
import Account from '../components/account';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, { accountId, added }) => ({
account: getAccount(state, accountId),
added: typeof added === 'undefined' ? state.getIn(['listEditor', 'accounts', 'items']).includes(accountId) : added,
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { accountId }) => ({
onRemove: () => dispatch(removeFromListEditor(accountId)),
onAdd: () => dispatch(addToListEditor(accountId)),
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));

View file

@ -1,18 +0,0 @@
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
import Search from '../components/search';
const mapStateToProps = state => ({
value: state.getIn(['listEditor', 'suggestions', 'value']),
});
const mapDispatchToProps = dispatch => ({
onSubmit: value => dispatch(fetchListSuggestions(value)),
onClear: () => dispatch(clearListSuggestions()),
onChange: value => dispatch(changeListSuggestions(value)),
});
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Search));

View file

@ -11,10 +11,9 @@ import spring from 'react-motion/lib/spring';
import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
import Motion from '../ui/util/optional_motion';
import Account from './components/account';
import EditListForm from './components/edit_list_form';
import AccountContainer from './containers/account_container';
import SearchContainer from './containers/search_container';
import Search from './components/search';
const mapStateToProps = state => ({
accountIds: state.getIn(['listEditor', 'accounts', 'items']),
@ -58,21 +57,21 @@ class ListEditor extends ImmutablePureComponent {
<div className='modal-root__modal list-editor'>
<EditListForm />
<SearchContainer />
<Search />
<div className='drawer__pager'>
<div className='drawer__inner list-editor__accounts'>
{accountIds.map(accountId => <AccountContainer key={accountId} accountId={accountId} added />)}
{accountIds.map(accountId => <Account key={accountId} accountId={accountId} added />)}
</div>
{showSearch && <div role='button' tabIndex={-1} className='drawer__backdrop' onClick={onClear} />}
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
{({ x }) =>
(<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
{searchAccountIds.map(accountId => <AccountContainer key={accountId} accountId={accountId} />)}
</div>)
}
{({ x }) => (
<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
</div>
)}
</Motion>
</div>
</div>

View file

@ -61,7 +61,7 @@ class Mutes extends ImmutablePureComponent {
const emptyMessage = <FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />;
return (
<Column bindToDocument={!multiColumn} name='mutes' icon='volume-off' heading={intl.formatMessage(messages.heading)}>
<Column bindToDocument={!multiColumn} icon='volume-off' heading={intl.formatMessage(messages.heading)}>
<ColumnBackButtonSlim />
<ScrollableList
scrollKey='mutes'

View file

@ -1,11 +1,11 @@
import PropTypes from 'prop-types';
import { Component } from 'react';
import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
import { Icon } from 'flavours/glitch/components/icon';
export default class ClearColumnButton extends Component {
export default class ClearColumnButton extends PureComponent {
static propTypes = {
onClick: PropTypes.func.isRequired,

View file

@ -5,7 +5,7 @@ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import AvatarOverlay from 'flavours/glitch/components/avatar_overlay';
import { AvatarOverlay } from 'flavours/glitch/components/avatar_overlay';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
// This needs to be kept in sync with app/models/report.rb

View file

@ -333,7 +333,6 @@ class Notifications extends PureComponent {
<Column
bindToDocument={!multiColumn}
ref={this.setColumnRef}
name='notifications'
extraClasses={this.props.notifCleaningActive ? 'notif-cleaning' : null}
label={intl.formatMessage(messages.title)}
>

View file

@ -13,7 +13,7 @@ import SwipeableViews from 'react-swipeable-views';
import Column from 'flavours/glitch/components/column';
import ColumnBackButton from 'flavours/glitch/components/column_back_button';
import { Icon } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { me, domain } from 'flavours/glitch/initial_state';
import ArrowSmallRight from './components/arrow_small_right';

View file

@ -122,8 +122,8 @@ class Footer extends ImmutablePureComponent {
}
};
_performReblog = (privacy) => {
const { dispatch, status } = this.props;
_performReblog = (status, privacy) => {
const { dispatch } = this.props;
dispatch(reblog(status, privacy));
};
@ -135,7 +135,7 @@ class Footer extends ImmutablePureComponent {
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else if ((e && e.shiftKey) || !boostModal) {
this._performReblog();
this._performReblog(status);
} else {
dispatch(initBoostModal({ status, onReblog: this._performReblog }));
}

View file

@ -133,7 +133,7 @@ class PublicTimeline extends PureComponent {
const pinned = !!columnId;
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} name='federated' label={intl.formatMessage(messages.title)}>
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='globe'
active={hasUnread}

View file

@ -12,7 +12,7 @@ import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
import Option from './option';
export default class StatusCheckBox extends PureComponent {
class StatusCheckBox extends PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
@ -40,7 +40,9 @@ export default class StatusCheckBox extends PureComponent {
<Avatar account={status.get('account')} size={46} />
</div>
<div><DisplayName account={status.get('account')} /> · <VisibilityIcon visibility={status.get('visibility')} /><RelativeTimestamp timestamp={status.get('created_at')} /></div>
<div>
<DisplayName account={status.get('account')} /> · <VisibilityIcon visibility={status.get('visibility')} /><RelativeTimestamp timestamp={status.get('created_at')} />
</div>
</div>
<StatusContent status={status} media={<MediaAttachments status={status} revealed={false} />} />
@ -61,3 +63,5 @@ export default class StatusCheckBox extends PureComponent {
}
}
export default StatusCheckBox;

View file

@ -61,13 +61,13 @@ class ActionBar extends PureComponent {
onFavourite: PropTypes.func.isRequired,
onReactionAdd: PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired,
onMute: PropTypes.func,
onMuteConversation: PropTypes.func,
onBlock: PropTypes.func,
onDelete: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onMute: PropTypes.func,
onBlock: PropTypes.func,
onMuteConversation: PropTypes.func,
onReport: PropTypes.func,
onPin: PropTypes.func,
onEmbed: PropTypes.func,
@ -119,14 +119,14 @@ class ActionBar extends PureComponent {
this.props.onMute(this.props.status.get('account'));
};
handleConversationMuteClick = () => {
this.props.onMuteConversation(this.props.status);
};
handleBlockClick = () => {
this.props.onBlock(this.props.status);
};
handleConversationMuteClick = () => {
this.props.onMuteConversation(this.props.status);
};
handleReport = () => {
this.props.onReport(this.props.status);
};

View file

@ -3,13 +3,13 @@ import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
import classnames from 'classnames';
import classNames from 'classnames';
import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { useBlurhash } from 'flavours/glitch/initial_state';
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
@ -148,7 +148,7 @@ export default class Card extends PureComponent {
const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
const horizontal = (!compact && card.get('width') > card.get('height')) || card.get('type') !== 'link' || embedded;
const interactive = card.get('type') !== 'link';
const className = classnames('status-card', { horizontal, compact, interactive });
const className = classNames('status-card', { horizontal, compact, interactive });
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
const language = card.get('language') || '';
@ -171,15 +171,17 @@ export default class Card extends PureComponent {
let embed = '';
let canvas = (
<Blurhash
className={classnames('status-card__image-preview', {
className={classNames('status-card__image-preview', {
'status-card__image-preview--hidden': revealed && this.state.previewLoaded,
})}
hash={card.get('blurhash')}
dummy={!useBlurhash}
/>
);
const thumbnailDescription = card.get('image_description');
const thumbnail = <img src={card.get('image')} alt={thumbnailDescription} title={thumbnailDescription} lang={language} style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
let spoilerButton = (
<button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
<span className='spoiler-button__overlay__label'>
@ -188,8 +190,9 @@ export default class Card extends PureComponent {
</span>
</button>
);
spoilerButton = (
<div className={classnames('spoiler-button', { 'spoiler-button--minified': revealed })}>
<div className={classNames('spoiler-button', { 'spoiler-button--minified': revealed })}>
{spoilerButton}
</div>
);
@ -209,15 +212,14 @@ export default class Card extends PureComponent {
{canvas}
{thumbnail}
{revealed && (
{revealed ? (
<div className='status-card__actions'>
<div>
<button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
<button type='button' onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>}
</div>
</div>
)}
{!revealed && spoilerButton}
) : spoilerButton}
</div>
);
}

View file

@ -12,7 +12,7 @@ import { AnimatedNumber } from 'flavours/glitch/components/animated_number';
import AttachmentList from 'flavours/glitch/components/attachment_list';
import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
import { getHashtagBarForStatus } from 'flavours/glitch/components/hashtag_bar';
import { Icon } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
import PollContainer from 'flavours/glitch/containers/poll_container';

View file

@ -726,7 +726,7 @@ class Status extends ImmutablePureComponent {
bookmark: this.handleHotkeyBookmark,
mention: this.handleHotkeyMention,
openProfile: this.handleHotkeyOpenProfile,
toggleSpoiler: this.handleToggleHidden,
toggleHidden: this.handleToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
};

View file

@ -1,10 +1,10 @@
import PropTypes from 'prop-types';
import { Component } from 'react';
import { PureComponent } from 'react';
const emptyComponent = () => null;
const noop = () => { };
class Bundle extends Component {
class Bundle extends PureComponent {
static propTypes = {
fetchComponent: PropTypes.func.isRequired,
@ -26,7 +26,7 @@ class Bundle extends Component {
onFetchFail: noop,
};
static cache = {};
static cache = new Map;
state = {
mod: undefined,
@ -51,6 +51,7 @@ class Bundle extends Component {
load = (props) => {
const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props;
const cachedMod = Bundle.cache.get(fetchComponent);
if (fetchComponent === undefined) {
this.setState({ mod: null });
@ -59,10 +60,8 @@ class Bundle extends Component {
onFetch();
if (Bundle.cache[fetchComponent.name]) {
const mod = Bundle.cache[fetchComponent.name];
this.setState({ mod: mod.default });
if (cachedMod) {
this.setState({ mod: cachedMod.default });
onFetchSuccess();
return Promise.resolve();
}
@ -76,7 +75,7 @@ class Bundle extends Component {
return fetchComponent()
.then((mod) => {
Bundle.cache[fetchComponent.name] = mod;
Bundle.cache.set(fetchComponent, mod);
this.setState({ mod: mod.default });
onFetchSuccess();
})

View file

@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
import { Component } from 'react';
import { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
@ -11,7 +11,7 @@ const messages = defineMessages({
close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' },
});
class BundleModalError extends Component {
class BundleModalError extends PureComponent {
static propTypes = {
onRetry: PropTypes.func.isRequired,

View file

@ -75,7 +75,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
}
componentWillUpdate(nextProps) {
UNSAFE_componentWillUpdate(nextProps) {
if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}

View file

@ -96,24 +96,10 @@ class MediaModal extends ImmutablePureComponent {
componentDidMount () {
window.addEventListener('keydown', this.handleKeyDown, false);
this._sendBackgroundColor();
}
componentWillUnmount () {
window.removeEventListener('keydown', this.handleKeyDown);
this.props.onChangeBackgroundColor(null);
}
getIndex () {
return this.state.index !== null ? this.state.index : this.props.index;
}
toggleNavigation = () => {
this.setState(prevState => ({
navigationHidden: !prevState.navigationHidden,
}));
};
componentDidUpdate (prevProps, prevState) {
if (prevState.index !== this.state.index) {
this._sendBackgroundColor();
@ -131,6 +117,22 @@ class MediaModal extends ImmutablePureComponent {
}
}
componentWillUnmount () {
window.removeEventListener('keydown', this.handleKeyDown);
this.props.onChangeBackgroundColor(null);
}
getIndex () {
return this.state.index !== null ? this.state.index : this.props.index;
}
toggleNavigation = () => {
this.setState(prevState => ({
navigationHidden: !prevState.navigationHidden,
}));
};
render () {
const { media, statusId, lang, intl, onClose } = this.props;
const { navigationHidden } = this.state;

View file

@ -48,7 +48,7 @@ class NavigationPanel extends Component {
return match || location.pathname.startsWith('/public');
};
render() {
render () {
const { intl, onOpenSettings } = this.props;
const { signedIn, disabledAccountId } = this.context.identity;

View file

@ -40,14 +40,14 @@ export default class UploadArea extends PureComponent {
return (
<Motion defaultStyle={{ backgroundOpacity: 0, backgroundScale: 0.95 }} style={{ backgroundOpacity: spring(active ? 1 : 0, { stiffness: 150, damping: 15 }), backgroundScale: spring(active ? 1 : 0.95, { stiffness: 200, damping: 3 }) }}>
{({ backgroundOpacity, backgroundScale }) =>
(<div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}>
{({ backgroundOpacity, backgroundScale }) => (
<div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}>
<div className='upload-area__drop'>
<div className='upload-area__background' style={{ transform: `scale(${backgroundScale})` }} />
<div className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div>
</div>
</div>)
}
</div>
)}
</Motion>
);
}

View file

@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
import { PureComponent, Component } from 'react';
import { PureComponent } from 'react';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
@ -68,9 +68,10 @@ import {
PrivacyPolicy,
} from './util/async-components';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
// Dummy import, to make sure that <Status /> ends up in the application bundle.
// Without this it ends up in ~8 very commonly used bundles.
import "../../components/status";
import '../../components/status';
const messages = defineMessages({
beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Mastodon.' },
@ -119,7 +120,7 @@ const keyMap = {
goToBlocked: 'g b',
goToMuted: 'g m',
goToRequests: 'g r',
toggleSpoiler: 'x',
toggleHidden: 'x',
bookmark: 'd',
toggleCollapse: 'shift+x',
toggleSensitive: 'h',
@ -255,7 +256,7 @@ class SwitchingColumnsArea extends PureComponent {
}
class UI extends Component {
class UI extends PureComponent {
static contextTypes = {
identity: PropTypes.object.isRequired,
@ -270,7 +271,6 @@ class UI extends Component {
hasComposingText: PropTypes.bool,
hasMediaAttachments: PropTypes.bool,
canUploadMore: PropTypes.bool,
match: PropTypes.object.isRequired,
intl: PropTypes.object.isRequired,
dropdownMenuIsOpen: PropTypes.bool,
unreadNotifications: PropTypes.number,
@ -287,7 +287,7 @@ class UI extends Component {
draggingOver: false,
};
handleBeforeUnload = (e) => {
handleBeforeUnload = e => {
const { intl, dispatch, hasComposingText, hasMediaAttachments } = this.props;
dispatch(synchronouslySubmitMarkers());
@ -300,6 +300,14 @@ class UI extends Component {
}
};
handleVisibilityChange = () => {
const visibility = !document[this.visibilityHiddenProp];
this.props.dispatch(notificationsSetVisibility(visibility));
if (visibility) {
this.props.dispatch(submitMarkers({ immediate: true }));
}
};
handleDragEnter = (e) => {
e.preventDefault();
@ -311,13 +319,14 @@ class UI extends Component {
this.dragTargets.push(e.target);
}
if (e.dataTransfer && e.dataTransfer.types.includes('Files') && this.props.canUploadMore && this.context.identity.signedIn) {
if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files') && this.props.canUploadMore && this.context.identity.signedIn) {
this.setState({ draggingOver: true });
}
};
handleDragOver = (e) => {
if (this.dataTransferIsText(e.dataTransfer)) return false;
e.preventDefault();
e.stopPropagation();
@ -372,14 +381,6 @@ class UI extends Component {
}
};
handleVisibilityChange = () => {
const visibility = !document[this.visibilityHiddenProp];
this.props.dispatch(notificationsSetVisibility(visibility));
if (visibility) {
this.props.dispatch(submitMarkers({ immediate: true }));
}
};
handleLayoutChange = debounce(() => {
this.props.dispatch(clearHeight()); // The cached heights are no longer accurate, invalidate
}, 500, {

View file

@ -85,10 +85,14 @@
display: block;
position: relative;
cursor: pointer;
width: 36px;
height: 36px;
background-size: 36px 36px;
overflow: hidden;
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
&-inline {
display: inline-block;
@ -102,7 +106,7 @@
overflow: hidden;
position: relative;
& div {
& > div {
@include avatar-radius;
float: left;
@ -110,6 +114,11 @@
box-sizing: border-box;
}
.account__avatar {
width: 100% !important;
height: 100% !important;
}
&__label {
display: block;
position: absolute;
@ -125,37 +134,13 @@
}
.account__avatar-overlay {
@include avatar-size(48px);
position: relative;
&-base {
@include avatar-radius;
@include avatar-size(36px);
img {
@include avatar-radius;
width: 100%;
height: 100%;
}
}
&-overlay {
@include avatar-radius;
@include avatar-size(24px);
position: absolute;
bottom: 0;
inset-inline-end: 0;
z-index: 1;
img {
@include avatar-radius;
width: 100%;
height: 100%;
}
}
}

View file

@ -701,8 +701,6 @@ a.status__display-name,
.status__avatar {
flex: none;
margin-inline-end: 10px;
height: 48px;
width: 48px;
}
.muted {
@ -1068,7 +1066,7 @@ a.status-card.compact:hover {
width: 0;
position: absolute;
top: 0;
inset-inline-start: 14px + ((48px - 2px) * 0.5);
inset-inline-start: 14px + ((46px - 2px) * 0.5);
&--full {
top: 0;
@ -1079,7 +1077,7 @@ a.status-card.compact:hover {
display: block;
position: absolute;
top: 10px - 4px;
height: 48px + 4px + 4px;
height: 46px + 4px + 4px;
width: 2px;
background: $ui-base-color;
inset-inline-start: -2px;
@ -1087,8 +1085,8 @@ a.status-card.compact:hover {
}
&--first {
top: 10px + 48px + 4px;
height: calc(100% - (10px + 48px + 4px));
top: 10px + 46px + 4px;
height: calc(100% - (10px + 46px + 4px));
&::before {
display: none;