@@ -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> | |||||
); | |||||
} | |||||
} |
@@ -1,25 +1,28 @@ | |||||
import React from 'react'; | import React from 'react'; | ||||
import ImmutablePropTypes from 'react-immutable-proptypes'; | import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
import PropTypes from 'prop-types'; | |||||
export default class DisplayName extends React.PureComponent { | export default class DisplayName extends React.PureComponent { | ||||
static propTypes = { | static propTypes = { | ||||
account: ImmutablePropTypes.map.isRequired, | account: ImmutablePropTypes.map.isRequired, | ||||
withAcct: PropTypes.bool, | |||||
}; | |||||
static defaultProps = { | |||||
withAcct: true, | |||||
others: ImmutablePropTypes.list, | |||||
}; | }; | ||||
render () { | render () { | ||||
const { account, withAcct } = this.props; | |||||
const { account, others } = this.props; | |||||
const displayNameHtml = { __html: account.get('display_name_html') }; | 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 ( | return ( | ||||
<span className='display-name'> | <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> | </span> | ||||
); | ); | ||||
} | } | ||||
@@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; | |||||
import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||
import Avatar from './avatar'; | import Avatar from './avatar'; | ||||
import AvatarOverlay from './avatar_overlay'; | import AvatarOverlay from './avatar_overlay'; | ||||
import AvatarComposite from './avatar_composite'; | |||||
import RelativeTimestamp from './relative_timestamp'; | import RelativeTimestamp from './relative_timestamp'; | ||||
import DisplayName from './display_name'; | import DisplayName from './display_name'; | ||||
import StatusContent from './status_content'; | import StatusContent from './status_content'; | ||||
@@ -45,6 +46,8 @@ class Status extends ImmutablePureComponent { | |||||
static propTypes = { | static propTypes = { | ||||
status: ImmutablePropTypes.map, | status: ImmutablePropTypes.map, | ||||
account: ImmutablePropTypes.map, | account: ImmutablePropTypes.map, | ||||
otherAccounts: ImmutablePropTypes.list, | |||||
onClick: PropTypes.func, | |||||
onReply: PropTypes.func, | onReply: PropTypes.func, | ||||
onFavourite: PropTypes.func, | onFavourite: PropTypes.func, | ||||
onReblog: PropTypes.func, | onReblog: PropTypes.func, | ||||
@@ -60,6 +63,7 @@ class Status extends ImmutablePureComponent { | |||||
onToggleHidden: PropTypes.func, | onToggleHidden: PropTypes.func, | ||||
muted: PropTypes.bool, | muted: PropTypes.bool, | ||||
hidden: PropTypes.bool, | hidden: PropTypes.bool, | ||||
unread: PropTypes.bool, | |||||
onMoveUp: PropTypes.func, | onMoveUp: PropTypes.func, | ||||
onMoveDown: PropTypes.func, | onMoveDown: PropTypes.func, | ||||
}; | }; | ||||
@@ -74,6 +78,11 @@ class Status extends ImmutablePureComponent { | |||||
] | ] | ||||
handleClick = () => { | handleClick = () => { | ||||
if (this.props.onClick) { | |||||
this.props.onClick(); | |||||
return; | |||||
} | |||||
if (!this.context.router) { | if (!this.context.router) { | ||||
return; | return; | ||||
} | } | ||||
@@ -158,7 +167,7 @@ class Status extends ImmutablePureComponent { | |||||
let media = null; | let media = null; | ||||
let statusAvatar, prepend, rebloggedByText; | let statusAvatar, prepend, rebloggedByText; | ||||
const { intl, hidden, featured } = this.props; | |||||
const { intl, hidden, featured, otherAccounts, unread } = this.props; | |||||
let { status, account, ...other } = 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} />; | statusAvatar = <Avatar account={status.get('account')} size={48} />; | ||||
}else{ | |||||
} else { | |||||
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />; | statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />; | ||||
} | } | ||||
@@ -269,10 +280,10 @@ class Status extends ImmutablePureComponent { | |||||
return ( | return ( | ||||
<HotKeys handlers={handlers}> | <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} | {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'> | <div className='status__info'> | ||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> | <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} | {statusAvatar} | ||||
</div> | </div> | ||||
<DisplayName account={status.get('account')} /> | |||||
<DisplayName account={status.get('account')} others={otherAccounts} /> | |||||
</a> | </a> | ||||
</div> | </div> | ||||
@@ -2,13 +2,7 @@ import React from 'react'; | |||||
import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||
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 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 { | export default class Conversation extends ImmutablePureComponent { | ||||
@@ -19,7 +13,7 @@ export default class Conversation extends ImmutablePureComponent { | |||||
static propTypes = { | static propTypes = { | ||||
conversationId: PropTypes.string.isRequired, | conversationId: PropTypes.string.isRequired, | ||||
accounts: ImmutablePropTypes.list.isRequired, | accounts: ImmutablePropTypes.list.isRequired, | ||||
lastStatus: ImmutablePropTypes.map.isRequired, | |||||
lastStatusId: PropTypes.string, | |||||
unread:PropTypes.bool.isRequired, | unread:PropTypes.bool.isRequired, | ||||
onMoveUp: PropTypes.func, | onMoveUp: PropTypes.func, | ||||
onMoveDown: PropTypes.func, | onMoveDown: PropTypes.func, | ||||
@@ -31,13 +25,13 @@ export default class Conversation extends ImmutablePureComponent { | |||||
return; | return; | ||||
} | } | ||||
const { lastStatus, unread, markRead } = this.props; | |||||
const { lastStatusId, unread, markRead } = this.props; | |||||
if (unread) { | if (unread) { | ||||
markRead(); | markRead(); | ||||
} | } | ||||
this.context.router.history.push(`/statuses/${lastStatus.get('id')}`); | |||||
this.context.router.history.push(`/statuses/${lastStatusId}`); | |||||
} | } | ||||
handleHotkeyMoveUp = () => { | handleHotkeyMoveUp = () => { | ||||
@@ -49,44 +43,20 @@ export default class Conversation extends ImmutablePureComponent { | |||||
} | } | ||||
render () { | render () { | ||||
const { accounts, lastStatus, lastAccount, unread } = this.props; | |||||
const { accounts, lastStatusId, unread } = this.props; | |||||
if (lastStatus === null) { | |||||
if (lastStatusId === null) { | |||||
return 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 ( | 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} | |||||
/> | |||||
); | ); | ||||
} | } | ||||
@@ -4,13 +4,11 @@ import { markConversationRead } from '../../../actions/conversations'; | |||||
const mapStateToProps = (state, { conversationId }) => { | const mapStateToProps = (state, { conversationId }) => { | ||||
const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId); | const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId); | ||||
const lastStatus = state.getIn(['statuses', conversation.get('last_status')], null); | |||||
return { | return { | ||||
accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)), | accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)), | ||||
unread: conversation.get('unread'), | unread: conversation.get('unread'), | ||||
lastStatus, | |||||
lastAccount: lastStatus === null ? null : state.getIn(['accounts', lastStatus.get('account')], null), | |||||
lastStatusId: conversation.get('last_status', null), | |||||
}; | }; | ||||
}; | }; | ||||
@@ -801,7 +801,7 @@ | |||||
padding: 8px 10px; | padding: 8px 10px; | ||||
padding-left: 68px; | padding-left: 68px; | ||||
position: relative; | position: relative; | ||||
min-height: 48px; | |||||
min-height: 54px; | |||||
border-bottom: 1px solid lighten($ui-base-color, 8%); | border-bottom: 1px solid lighten($ui-base-color, 8%); | ||||
cursor: default; | cursor: default; | ||||
@@ -823,7 +823,7 @@ | |||||
margin-top: 8px; | margin-top: 8px; | ||||
} | } | ||||
&.status-direct { | |||||
&.status-direct:not(.read) { | |||||
background: lighten($ui-base-color, 8%); | background: lighten($ui-base-color, 8%); | ||||
border-bottom-color: lighten($ui-base-color, 12%); | border-bottom-color: lighten($ui-base-color, 12%); | ||||
} | } | ||||
@@ -1133,6 +1133,18 @@ | |||||
vertical-align: middle; | vertical-align: middle; | ||||
margin-right: 5px; | margin-right: 5px; | ||||
} | } | ||||
&-composite { | |||||
@include avatar-radius(); | |||||
overflow: hidden; | |||||
& > div { | |||||
@include avatar-radius(); | |||||
float: left; | |||||
position: relative; | |||||
box-sizing: border-box; | |||||
} | |||||
} | |||||
} | } | ||||
a .account__avatar { | 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; | |||||
} | |||||
} |