--- /dev/null
+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>
+ );
+ }
+
+}
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>
);
}
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';
static propTypes = {
status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map,
+ otherAccounts: ImmutablePropTypes.list,
+ onClick: PropTypes.func,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onToggleHidden: PropTypes.func,
muted: PropTypes.bool,
hidden: PropTypes.bool,
+ unread: PropTypes.bool,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
};
]
handleClick = () => {
+ if (this.props.onClick) {
+ this.props.onClick();
+ return;
+ }
+
if (!this.context.router) {
return;
}
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;
}
}
- 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} />;
}
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>
{statusAvatar}
</div>
- <DisplayName account={status.get('account')} />
+ <DisplayName account={status.get('account')} others={otherAccounts} />
</a>
</div>
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 {
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,
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 = () => {
}
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}
+ />
);
}
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),
};
};
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;
margin-top: 8px;
}
- &.status-direct {
+ &.status-direct:not(.read) {
background: lighten($ui-base-color, 8%);
border-bottom-color: lighten($ui-base-color, 12%);
}
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 {
}
}
}
-
-.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;
- }
-}