diff options
author | Eugen Rochko <eugen@zeonfederated.com> | 2018-10-20 02:23:58 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-10-20 02:23:58 +0200 |
commit | eb1b9903a6f60d024d71bffd635e6fec7edc59a9 (patch) | |
tree | 4bdb567460b53a0f022a522193309f8e3750405d | |
parent | 029943d59b222e96a2f7839390a5628888249bf6 (diff) |
Redesign direct messages column (#9022)
6 files changed, 152 insertions, 108 deletions
diff --git a/app/javascript/mastodon/components/avatar_composite.js b/app/javascript/mastodon/components/avatar_composite.js new file mode 100644 index 000000000..4a9a73c51 --- /dev/null +++ b/app/javascript/mastodon/components/avatar_composite.js @@ -0,0 +1,96 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { autoPlayGif } from '../initial_state'; + +export default class AvatarComposite extends React.PureComponent { + + static propTypes = { + accounts: ImmutablePropTypes.list.isRequired, + animate: PropTypes.bool, + size: PropTypes.number.isRequired, + }; + + static defaultProps = { + animate: autoPlayGif, + }; + + renderItem (account, size, index) { + const { animate } = this.props; + + let width = 50; + let height = 100; + let top = 'auto'; + let left = 'auto'; + let bottom = 'auto'; + let right = 'auto'; + + if (size === 1) { + width = 100; + } + + if (size === 4 || (size === 3 && index > 0)) { + height = 50; + } + + if (size === 2) { + if (index === 0) { + right = '2px'; + } else { + left = '2px'; + } + } else if (size === 3) { + if (index === 0) { + right = '2px'; + } else if (index > 0) { + left = '2px'; + } + + if (index === 1) { + bottom = '2px'; + } else if (index > 1) { + top = '2px'; + } + } else if (size === 4) { + if (index === 0 || index === 2) { + right = '2px'; + } + + if (index === 1 || index === 3) { + left = '2px'; + } + + if (index < 2) { + bottom = '2px'; + } else { + top = '2px'; + } + } + + const style = { + left: left, + top: top, + right: right, + bottom: bottom, + width: `${width}%`, + height: `${height}%`, + backgroundSize: 'cover', + backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`, + }; + + return ( + <div key={account.get('id')} style={style} /> + ); + } + + render() { + const { accounts, size } = this.props; + + return ( + <div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}> + {accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))} + </div> + ); + } + +} diff --git a/app/javascript/mastodon/components/display_name.js b/app/javascript/mastodon/components/display_name.js index c3a9ab921..c2c40cb3f 100644 --- a/app/javascript/mastodon/components/display_name.js +++ b/app/javascript/mastodon/components/display_name.js @@ -1,25 +1,28 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; export default class DisplayName extends React.PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, - withAcct: PropTypes.bool, - }; - - static defaultProps = { - withAcct: true, + others: ImmutablePropTypes.list, }; render () { - const { account, withAcct } = this.props; + const { account, others } = this.props; const displayNameHtml = { __html: account.get('display_name_html') }; + let suffix; + + if (others && others.size > 1) { + suffix = `+${others.size}`; + } else { + suffix = <span className='display-name__account'>@{account.get('acct')}</span>; + } + return ( <span className='display-name'> - <bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> {withAcct && <span className='display-name__account'>@{account.get('acct')}</span>} + <bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> {suffix} </span> ); } diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 90c689a75..0b23e51f8 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import Avatar from './avatar'; import AvatarOverlay from './avatar_overlay'; +import AvatarComposite from './avatar_composite'; import RelativeTimestamp from './relative_timestamp'; import DisplayName from './display_name'; import StatusContent from './status_content'; @@ -45,6 +46,8 @@ class Status extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map, account: ImmutablePropTypes.map, + otherAccounts: ImmutablePropTypes.list, + onClick: PropTypes.func, onReply: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, @@ -60,6 +63,7 @@ class Status extends ImmutablePureComponent { onToggleHidden: PropTypes.func, muted: PropTypes.bool, hidden: PropTypes.bool, + unread: PropTypes.bool, onMoveUp: PropTypes.func, onMoveDown: PropTypes.func, }; @@ -74,6 +78,11 @@ class Status extends ImmutablePureComponent { ] handleClick = () => { + if (this.props.onClick) { + this.props.onClick(); + return; + } + if (!this.context.router) { return; } @@ -158,7 +167,7 @@ class Status extends ImmutablePureComponent { let media = null; let statusAvatar, prepend, rebloggedByText; - const { intl, hidden, featured } = this.props; + const { intl, hidden, featured, otherAccounts, unread } = this.props; let { status, account, ...other } = this.props; @@ -249,9 +258,11 @@ class Status extends ImmutablePureComponent { } } - if (account === undefined || account === null) { + if (otherAccounts) { + statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} />; + } else if (account === undefined || account === null) { statusAvatar = <Avatar account={status.get('account')} size={48} />; - }else{ + } else { statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />; } @@ -269,10 +280,10 @@ class Status extends ImmutablePureComponent { return ( <HotKeys handlers={handlers}> - <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}> + <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}> {prepend} - <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} data-id={status.get('id')}> + <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}> <div className='status__info'> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> @@ -281,7 +292,7 @@ class Status extends ImmutablePureComponent { {statusAvatar} </div> - <DisplayName account={status.get('account')} /> + <DisplayName account={status.get('account')} others={otherAccounts} /> </a> </div> diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.js b/app/javascript/mastodon/features/direct_timeline/components/conversation.js index 52e33c3c8..7277b7f0f 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.js +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.js @@ -2,13 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import StatusContent from '../../../components/status_content'; -import RelativeTimestamp from '../../../components/relative_timestamp'; -import DisplayName from '../../../components/display_name'; -import Avatar from '../../../components/avatar'; -import AttachmentList from '../../../components/attachment_list'; -import { HotKeys } from 'react-hotkeys'; -import classNames from 'classnames'; +import StatusContainer from '../../../containers/status_container'; export default class Conversation extends ImmutablePureComponent { @@ -19,7 +13,7 @@ export default class Conversation extends ImmutablePureComponent { static propTypes = { conversationId: PropTypes.string.isRequired, accounts: ImmutablePropTypes.list.isRequired, - lastStatus: ImmutablePropTypes.map.isRequired, + lastStatusId: PropTypes.string, unread:PropTypes.bool.isRequired, onMoveUp: PropTypes.func, onMoveDown: PropTypes.func, @@ -31,13 +25,13 @@ export default class Conversation extends ImmutablePureComponent { return; } - const { lastStatus, unread, markRead } = this.props; + const { lastStatusId, unread, markRead } = this.props; if (unread) { markRead(); } - this.context.router.history.push(`/statuses/${lastStatus.get('id')}`); + this.context.router.history.push(`/statuses/${lastStatusId}`); } handleHotkeyMoveUp = () => { @@ -49,44 +43,20 @@ export default class Conversation extends ImmutablePureComponent { } render () { - const { accounts, lastStatus, lastAccount, unread } = this.props; + const { accounts, lastStatusId, unread } = this.props; - if (lastStatus === null) { + if (lastStatusId === null) { return null; } - const handlers = { - moveDown: this.handleHotkeyMoveDown, - moveUp: this.handleHotkeyMoveUp, - open: this.handleClick, - }; - - let media; - - if (lastStatus.get('media_attachments').size > 0) { - media = <AttachmentList compact media={lastStatus.get('media_attachments')} />; - } - return ( - <HotKeys handlers={handlers}> - <div className={classNames('conversation', 'focusable', { 'conversation--unread': unread })} tabIndex='0' onClick={this.handleClick} role='button'> - <div className='conversation__header'> - <div className='conversation__avatars'> - <div>{accounts.map(account => <Avatar key={account.get('id')} size={36} account={account} />)}</div> - </div> - - <div className='conversation__time'> - <RelativeTimestamp timestamp={lastStatus.get('created_at')} /> - <br /> - <DisplayName account={lastAccount} withAcct={false} /> - </div> - </div> - - <StatusContent status={lastStatus} onClick={this.handleClick} /> - - {media} - </div> - </HotKeys> + <StatusContainer + id={lastStatusId} + unread={unread} + otherAccounts={accounts} + onMoveUp={this.handleHotkeyMoveUp} + onMoveDown={this.handleHotkeyMoveDown} + /> ); } diff --git a/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js b/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js index e2e2e3afb..bd6f6bfb0 100644 --- a/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js +++ b/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js @@ -4,13 +4,11 @@ import { markConversationRead } from '../../../actions/conversations'; const mapStateToProps = (state, { conversationId }) => { const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId); - const lastStatus = state.getIn(['statuses', conversation.get('last_status')], null); return { accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)), unread: conversation.get('unread'), - lastStatus, - lastAccount: lastStatus === null ? null : state.getIn(['accounts', lastStatus.get('account')], null), + lastStatusId: conversation.get('last_status', null), }; }; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 24b614a37..f77dc405c 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -801,7 +801,7 @@ padding: 8px 10px; padding-left: 68px; position: relative; - min-height: 48px; + min-height: 54px; border-bottom: 1px solid lighten($ui-base-color, 8%); cursor: default; @@ -823,7 +823,7 @@ margin-top: 8px; } - &.status-direct { + &.status-direct:not(.read) { background: lighten($ui-base-color, 8%); border-bottom-color: lighten($ui-base-color, 12%); } @@ -1133,6 +1133,18 @@ vertical-align: middle; margin-right: 5px; } + + &-composite { + @include avatar-radius(); + overflow: hidden; + + & > div { + @include avatar-radius(); + float: left; + position: relative; + box-sizing: border-box; + } + } } a .account__avatar { @@ -5497,49 +5509,3 @@ noscript { } } } - -.conversation { - padding: 14px 10px; - border-bottom: 1px solid lighten($ui-base-color, 8%); - cursor: pointer; - - &--unread { - background: lighten($ui-base-color, 8%); - border-bottom-color: lighten($ui-base-color, 12%); - } - - &__header { - display: flex; - margin-bottom: 15px; - } - - &__avatars { - overflow: hidden; - flex: 1 1 auto; - - & > div { - display: flex; - flex-wrap: none; - width: 900px; - } - - .account__avatar { - margin-right: 10px; - } - } - - &__time { - flex: 0 0 auto; - font-size: 14px; - color: $darker-text-color; - text-align: right; - - .display-name { - color: $secondary-text-color; - } - } - - .attachment-list.compact { - margin-top: 15px; - } -} |