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

View file

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

View file

@ -5,49 +5,44 @@ import { autoPlayGif } from '../initial_state';
import type { Account } from '../types/resources'; import type { Account } from '../types/resources';
interface Props { interface Props {
account: Account | undefined; account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
className?: string;
size: number; size: number;
style?: React.CSSProperties; style?: React.CSSProperties;
inline?: boolean; inline?: boolean;
animate?: boolean;
} }
export const Avatar: React.FC<Props> = ({ export const Avatar: React.FC<Props> = ({
account, account,
className, animate = autoPlayGif,
size = 20, size = 20,
inline = false, inline = false,
style: styleFromParent, style: styleFromParent,
}) => { }) => {
const { hovering, handleMouseEnter, handleMouseLeave } = const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
useHovering(autoPlayGif);
const style = { const style = {
...styleFromParent, ...styleFromParent,
width: `${size}px`, width: `${size}px`,
height: `${size}px`, height: `${size}px`,
backgroundSize: `${size}px ${size}px`,
}; };
if (account) { const src =
style.backgroundImage = `url(${account.get( hovering || animate
hovering ? 'avatar' : 'avatar_static', ? account?.get('avatar')
)})`; : account?.get('avatar_static');
}
return ( return (
<div <div
className={classNames( className={classNames('account__avatar', {
'account__avatar', 'account__avatar-inline': inline,
{ 'account__avatar-inline': inline }, })}
className,
)}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
style={style} style={style}
data-avatar-of={account && `@${account.get('acct')}`} 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 { autoPlayGif } from '../initial_state';
import { Avatar } from './avatar';
export default class AvatarComposite extends PureComponent { export default class AvatarComposite extends PureComponent {
static propTypes = { static propTypes = {
@ -76,12 +78,12 @@ export default class AvatarComposite extends PureComponent {
bottom: bottom, bottom: bottom,
width: `${width}%`, width: `${width}%`,
height: `${height}%`, height: `${height}%`,
backgroundSize: 'cover',
backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
}; };
return ( 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 = { static propTypes = {
children: PropTypes.node, children: PropTypes.node,
extraClasses: PropTypes.string, extraClasses: PropTypes.string,
name: PropTypes.string,
label: PropTypes.string, label: PropTypes.string,
bindToDocument: PropTypes.bool, bindToDocument: PropTypes.bool,
}; };
@ -62,10 +61,10 @@ export default class Column extends PureComponent {
} }
render () { render () {
const { children, extraClasses, name, label } = this.props; const { label, children, extraClasses } = this.props;
return ( 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} {children}
</div> </div>
); );

View file

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

View file

@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
// Mastodon imports. // Mastodon imports.
import { Avatar } from './avatar'; import { Avatar } from './avatar';
import AvatarOverlay from './avatar_overlay'; import { AvatarOverlay } from './avatar_overlay';
import { DisplayName } from './display_name'; import { DisplayName } from './display_name';
export default class StatusHeader extends PureComponent { export default class StatusHeader extends PureComponent {
@ -39,7 +39,7 @@ export default class StatusHeader extends PureComponent {
let statusAvatar; let statusAvatar;
if (friend === undefined || friend === null) { if (friend === undefined || friend === null) {
statusAvatar = <Avatar account={account} size={48} />; statusAvatar = <Avatar account={account} size={46} />;
} else { } else {
statusAvatar = <AvatarOverlay account={account} friend={friend} />; 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 title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
const hydrateAction = hydrateStore(initialState); const hydrateAction = hydrateStore(initialState);
store.dispatch(hydrateAction); store.dispatch(hydrateAction);
// check for deprecated local settings // check for deprecated local settings
@ -71,8 +72,8 @@ export default class Mastodon extends PureComponent {
} }
} }
shouldUpdateScroll (_, { location }) { shouldUpdateScroll (prevRouterProps, { location }) {
return !(location.state?.mastodonModalKey); return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey);
} }
render () { render () {

View file

@ -38,7 +38,7 @@ class FeaturedTags extends ImmutablePureComponent {
name={featuredTag.get('name')} name={featuredTag.get('name')}
href={featuredTag.get('url')} href={featuredTag.get('url')}
to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`} to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`}
uses={featuredTag.get('statuses_count')} uses={featuredTag.get('statuses_count') * 1}
withGraph={false} 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)} 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 { Avatar } from 'flavours/glitch/components/avatar';
import { Button } from 'flavours/glitch/components/button'; 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 { IconButton } from 'flavours/glitch/components/icon_button';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state'; 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'))) { if (['audio', 'video'].includes(attachment.get('type'))) {
content = ( content = (
<img <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')} alt={attachment.get('description')}
lang={status.get('language')} lang={status.get('language')}
onLoad={this.handleImageLoad} onLoad={this.handleImageLoad}

View file

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

View file

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

View file

@ -9,7 +9,7 @@ import { is } from 'immutable';
import { throttle, debounce } from 'lodash'; 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 { formatTime, getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video';
import { Blurhash } from '../../components/blurhash'; 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." />; const emptyMessage = <FormattedMessage id='empty_column.blocks' defaultMessage="You haven't blocked any users yet." />;
return ( return (
<Column name='blocks' bindToDocument={!multiColumn} icon='ban' heading={intl.formatMessage(messages.heading)}> <Column bindToDocument={!multiColumn} icon='ban' heading={intl.formatMessage(messages.heading)}>
<ColumnBackButtonSlim /> <ColumnBackButtonSlim />
<ScrollableList <ScrollableList
scrollKey='blocks' 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." />; 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 ( return (
<Column bindToDocument={!multiColumn} ref={this.setRef} name='bookmarks'> <Column bindToDocument={!multiColumn} ref={this.setRef}>
<ColumnHeader <ColumnHeader
icon='bookmark' icon='bookmark'
title={intl.formatMessage(messages.heading)} title={intl.formatMessage(messages.heading)}

View file

@ -40,14 +40,14 @@ const mapStateToProps = (state, { columnId }) => {
class CommunityTimeline extends PureComponent { class CommunityTimeline extends PureComponent {
static defaultProps = {
onlyMedia: false,
};
static contextTypes = { static contextTypes = {
identity: PropTypes.object, identity: PropTypes.object,
}; };
static defaultProps = {
onlyMedia: false,
};
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string, columnId: PropTypes.string,
@ -128,7 +128,7 @@ class CommunityTimeline extends PureComponent {
const pinned = !!columnId; const pinned = !!columnId;
return ( 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 <ColumnHeader
icon='users' icon='users'
active={hasUnread} active={hasUnread}

View file

@ -17,19 +17,21 @@ export default class NavigationBar extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired,
onLogout: PropTypes.func.isRequired, onLogout: PropTypes.func.isRequired,
onClose: PropTypes.func,
}; };
render () { render () {
const username = this.props.account.get('acct');
return ( return (
<div className='navigation-bar'> <div className='navigation-bar'>
<Permalink className='avatar' href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}> <Permalink className='avatar' href={this.props.account.get('url')} to={`/@${username}`}>
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span> <span style={{ display: 'none' }}>{username}</span>
<Avatar account={this.props.account} size={48} /> <Avatar account={this.props.account} size={46} />
</Permalink> </Permalink>
<div className='navigation-bar__profile'> <div className='navigation-bar__profile'>
<Permalink className='acct' href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}> <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> </Permalink>
{ profileLink !== undefined && ( { 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) => { handleKeyDown = (e) => {
const { selectedOption } = this.state; const { selectedOption } = this.state;
const options = searchEnabled ? this._getOptions().concat(this.defaultOptions) : this._getOptions(); const options = searchEnabled ? this._getOptions().concat(this.defaultOptions) : this._getOptions();
@ -161,8 +142,23 @@ class Search extends PureComponent {
} }
}; };
findTarget = () => { handleFocus = () => {
return this.searchForm; 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 = () => { handleHashtagClick = () => {

View file

@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; 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 { LoadMore } from 'flavours/glitch/components/load_more';
import { SearchSection } from 'flavours/glitch/features/explore/components/search_section'; import { SearchSection } from 'flavours/glitch/features/explore/components/search_section';
@ -69,6 +69,7 @@ class SearchResults extends ImmutablePureComponent {
); );
} }
return ( return (
<div className='drawer--results'> <div className='drawer--results'>
<header className='search-results__header'> <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." />; 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 ( 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 <ColumnHeader
icon='star' icon='star'
title={intl.formatMessage(messages.heading)} title={intl.formatMessage(messages.heading)}

View file

@ -85,9 +85,10 @@ class Favourites extends ImmutablePureComponent {
showBackButton showBackButton
multiColumn={multiColumn} multiColumn={multiColumn}
extraButton={( 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 <ScrollableList
scrollKey='favourites' scrollKey='favourites'
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}

View file

@ -67,7 +67,7 @@ class FollowRequests extends ImmutablePureComponent {
); );
return ( 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 /> <ColumnBackButtonSlim />
<ScrollableList <ScrollableList
scrollKey='follow_requests' scrollKey='follow_requests'

View file

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

View file

@ -173,7 +173,7 @@ class GettingStarted extends ImmutablePureComponent {
} }
return ( 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='scrollable optionally-scrollable'>
<div className='getting-started__wrapper'> <div className='getting-started__wrapper'>
{!multiColumn && signedIn && <NavigationBar account={myAccount} />} {!multiColumn && signedIn && <NavigationBar account={myAccount} />}

View file

@ -196,7 +196,7 @@ class HomeTimeline extends PureComponent {
} }
return ( 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 <ColumnHeader
icon='home' icon='home'
active={hasUnread} active={hasUnread}

View file

@ -16,7 +16,7 @@ const messages = defineMessages({
add: { id: 'lists.account.add', defaultMessage: 'Add to list' }, 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), list: state.get('lists').get(listId),
added: typeof added === 'undefined' ? state.getIn(['listAdder', 'lists', 'items']).includes(listId) : added, 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 PropTypes from 'prop-types';
import { defineMessages } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { removeFromListEditor, addToListEditor } from '../../../actions/lists';
import { Avatar } from '../../../components/avatar'; import { Avatar } from '../../../components/avatar';
import { DisplayName } from '../../../components/display_name'; import { DisplayName } from '../../../components/display_name';
import { IconButton } from '../../../components/icon_button'; import { IconButton } from '../../../components/icon_button';
import { makeGetAccount } from '../../../selectors';
const messages = defineMessages({ const messages = defineMessages({
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' }, remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
add: { id: 'lists.account.add', defaultMessage: 'Add to 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 = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, 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 PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { defineMessages } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { connect } from 'react-redux';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
const messages = defineMessages({ const messages = defineMessages({
search: { id: 'lists.search', defaultMessage: 'Search among people you follow' }, 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 = { static propTypes = {
intl: PropTypes.object.isRequired, 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 { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
import Motion from '../ui/util/optional_motion'; import Motion from '../ui/util/optional_motion';
import Account from './components/account';
import EditListForm from './components/edit_list_form'; import EditListForm from './components/edit_list_form';
import AccountContainer from './containers/account_container'; import Search from './components/search';
import SearchContainer from './containers/search_container';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
accountIds: state.getIn(['listEditor', 'accounts', 'items']), accountIds: state.getIn(['listEditor', 'accounts', 'items']),
@ -58,21 +57,21 @@ class ListEditor extends ImmutablePureComponent {
<div className='modal-root__modal list-editor'> <div className='modal-root__modal list-editor'>
<EditListForm /> <EditListForm />
<SearchContainer /> <Search />
<div className='drawer__pager'> <div className='drawer__pager'>
<div className='drawer__inner list-editor__accounts'> <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> </div>
{showSearch && <div role='button' tabIndex={-1} className='drawer__backdrop' onClick={onClear} />} {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 }) }}> <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
{({ x }) => {({ x }) => (
(<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}> <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} />)} {searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
</div>) </div>
} )}
</Motion> </Motion>
</div> </div>
</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." />; const emptyMessage = <FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />;
return ( 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 /> <ColumnBackButtonSlim />
<ScrollableList <ScrollableList
scrollKey='mutes' scrollKey='mutes'

View file

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

View file

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

View file

@ -333,7 +333,6 @@ class Notifications extends PureComponent {
<Column <Column
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
ref={this.setColumnRef} ref={this.setColumnRef}
name='notifications'
extraClasses={this.props.notifCleaningActive ? 'notif-cleaning' : null} extraClasses={this.props.notifCleaningActive ? 'notif-cleaning' : null}
label={intl.formatMessage(messages.title)} 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 Column from 'flavours/glitch/components/column';
import ColumnBackButton from 'flavours/glitch/components/column_back_button'; 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 { me, domain } from 'flavours/glitch/initial_state';
import ArrowSmallRight from './components/arrow_small_right'; import ArrowSmallRight from './components/arrow_small_right';

View file

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

View file

@ -133,7 +133,7 @@ class PublicTimeline extends PureComponent {
const pinned = !!columnId; const pinned = !!columnId;
return ( 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 <ColumnHeader
icon='globe' icon='globe'
active={hasUnread} active={hasUnread}

View file

@ -12,7 +12,7 @@ import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
import Option from './option'; import Option from './option';
export default class StatusCheckBox extends PureComponent { class StatusCheckBox extends PureComponent {
static propTypes = { static propTypes = {
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
@ -40,7 +40,9 @@ export default class StatusCheckBox extends PureComponent {
<Avatar account={status.get('account')} size={46} /> <Avatar account={status.get('account')} size={46} />
</div> </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> </div>
<StatusContent status={status} media={<MediaAttachments status={status} revealed={false} />} /> <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, onFavourite: PropTypes.func.isRequired,
onReactionAdd: PropTypes.func.isRequired, onReactionAdd: PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired, onBookmark: PropTypes.func.isRequired,
onMute: PropTypes.func,
onMuteConversation: PropTypes.func,
onBlock: PropTypes.func,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired, onEdit: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired, onDirect: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired,
onMute: PropTypes.func,
onBlock: PropTypes.func,
onMuteConversation: PropTypes.func,
onReport: PropTypes.func, onReport: PropTypes.func,
onPin: PropTypes.func, onPin: PropTypes.func,
onEmbed: PropTypes.func, onEmbed: PropTypes.func,
@ -119,14 +119,14 @@ class ActionBar extends PureComponent {
this.props.onMute(this.props.status.get('account')); this.props.onMute(this.props.status.get('account'));
}; };
handleConversationMuteClick = () => {
this.props.onMuteConversation(this.props.status);
};
handleBlockClick = () => { handleBlockClick = () => {
this.props.onBlock(this.props.status); this.props.onBlock(this.props.status);
}; };
handleConversationMuteClick = () => {
this.props.onMuteConversation(this.props.status);
};
handleReport = () => { handleReport = () => {
this.props.onReport(this.props.status); this.props.onReport(this.props.status);
}; };

View file

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

View file

@ -12,7 +12,7 @@ import { AnimatedNumber } from 'flavours/glitch/components/animated_number';
import AttachmentList from 'flavours/glitch/components/attachment_list'; import AttachmentList from 'flavours/glitch/components/attachment_list';
import EditedTimestamp from 'flavours/glitch/components/edited_timestamp'; import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
import { getHashtagBarForStatus } from 'flavours/glitch/components/hashtag_bar'; 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 PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon'; import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
import PollContainer from 'flavours/glitch/containers/poll_container'; import PollContainer from 'flavours/glitch/containers/poll_container';

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Component } from 'react'; import { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
@ -11,7 +11,7 @@ const messages = defineMessages({
close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' }, close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' },
}); });
class BundleModalError extends Component { class BundleModalError extends PureComponent {
static propTypes = { static propTypes = {
onRetry: PropTypes.func.isRequired, 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'); this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
} }
componentWillUpdate(nextProps) { UNSAFE_componentWillUpdate(nextProps) {
if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) { if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel); this.node.removeEventListener('wheel', this.handleWheel);
} }

View file

@ -96,24 +96,10 @@ class MediaModal extends ImmutablePureComponent {
componentDidMount () { componentDidMount () {
window.addEventListener('keydown', this.handleKeyDown, false); window.addEventListener('keydown', this.handleKeyDown, false);
this._sendBackgroundColor(); 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) { componentDidUpdate (prevProps, prevState) {
if (prevState.index !== this.state.index) { if (prevState.index !== this.state.index) {
this._sendBackgroundColor(); 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 () { render () {
const { media, statusId, lang, intl, onClose } = this.props; const { media, statusId, lang, intl, onClose } = this.props;
const { navigationHidden } = this.state; const { navigationHidden } = this.state;

View file

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

View file

@ -40,14 +40,14 @@ export default class UploadArea extends PureComponent {
return ( 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 }) }}> <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 }) => {({ backgroundOpacity, backgroundScale }) => (
(<div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}> <div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}>
<div className='upload-area__drop'> <div className='upload-area__drop'>
<div className='upload-area__background' style={{ transform: `scale(${backgroundScale})` }} /> <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 className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div>
</div> </div>
</div>) </div>
} )}
</Motion> </Motion>
); );
} }

View file

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

View file

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

View file

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