diff options
Diffstat (limited to 'app/javascript/flavours/glitch/components')
6 files changed, 229 insertions, 50 deletions
diff --git a/app/javascript/flavours/glitch/components/avatar_composite.js b/app/javascript/flavours/glitch/components/avatar_composite.js new file mode 100644 index 000000000..c52df043a --- /dev/null +++ b/app/javascript/flavours/glitch/components/avatar_composite.js @@ -0,0 +1,104 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { autoPlayGif } from 'flavours/glitch/util/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 ( + <a + href={account.get('url')} + target='_blank' + onClick={(e) => this.props.onAccountClick(account.get('id'), e)} + title={`@${account.get('acct')}`} + key={account.get('id')} + > + <div style={style} data-avatar-of={`@${account.get('acct')}`} /> + </a> + ); + } + + 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/flavours/glitch/components/display_name.js b/app/javascript/flavours/glitch/components/display_name.js index a26cff049..7f6ef5a5d 100644 --- a/app/javascript/flavours/glitch/components/display_name.js +++ b/app/javascript/flavours/glitch/components/display_name.js @@ -10,24 +10,56 @@ export default function DisplayName ({ className, inline, localDomain, + others, + onAccountClick, }) { const computedClass = classNames('display-name', { inline }, className); if (!account) return null; + let displayName, suffix; + let acct = account.get('acct'); + if (acct.indexOf('@') === -1 && localDomain) { acct = `${acct}@${localDomain}`; } - // The result. - return account ? ( + if (others && others.size > 0) { + displayName = others.take(2).map(a => ( + <a + href={a.get('url')} + target='_blank' + onClick={(e) => onAccountClick(a.get('id'), e)} + title={`@${a.get('acct')}`} + > + <bdi key={a.get('id')}> + <strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /> + </bdi> + </a> + )).reduce((prev, cur) => [prev, ', ', cur]); + + if (others.size - 2 > 0) { + displayName.push(` +${others.size - 2}`); + } + + suffix = ( + <a href={account.get('url')} target='_blank' onClick={(e) => onAccountClick(account.get('id'), e)}> + <span className='display-name__account'>@{acct}</span> + </a> + ); + } else { + displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>; + suffix = <span className='display-name__account'>@{acct}</span>; + } + + return ( <span className={computedClass}> - <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi> + {displayName} {inline ? ' ' : null} - <span className='display-name__account'>@{acct}</span> + {suffix} </span> - ) : null; + ); } // Props. @@ -36,4 +68,6 @@ DisplayName.propTypes = { className: PropTypes.string, inline: PropTypes.bool, localDomain: PropTypes.string, + others: ImmutablePropTypes.list, + handleClick: PropTypes.func, }; diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 7014cab17..4b9364ae5 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -66,6 +66,7 @@ export default class Status extends ImmutablePureComponent { containerId: PropTypes.string, id: PropTypes.string, status: ImmutablePropTypes.map, + otherAccounts: ImmutablePropTypes.list, account: ImmutablePropTypes.map, onReply: PropTypes.func, onFavourite: PropTypes.func, @@ -83,6 +84,7 @@ export default class Status extends ImmutablePureComponent { muted: PropTypes.bool, collapse: PropTypes.bool, hidden: PropTypes.bool, + unread: PropTypes.bool, prepend: PropTypes.string, withDismiss: PropTypes.bool, onMoveUp: PropTypes.func, @@ -93,6 +95,7 @@ export default class Status extends ImmutablePureComponent { intl: PropTypes.object.isRequired, cacheMediaWidth: PropTypes.func, cachedMediaWidth: PropTypes.number, + onClick: PropTypes.func, }; state = { @@ -321,17 +324,21 @@ export default class Status extends ImmutablePureComponent { const { status } = this.props; const { isCollapsed } = this.state; if (!router) return; - if (destination === undefined) { - destination = `/statuses/${ - status.getIn(['reblog', 'id'], status.get('id')) - }`; - } + if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) { if (isCollapsed) this.setCollapsed(false); else if (e.shiftKey) { this.setCollapsed(true); document.getSelection().removeAllRanges(); + } else if (this.props.onClick) { + this.props.onClick(); + return; } else { + if (destination === undefined) { + destination = `/statuses/${ + status.getIn(['reblog', 'id'], status.get('id')) + }`; + } let state = {...router.history.location.state}; state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; router.history.push(destination, state); @@ -441,6 +448,7 @@ export default class Status extends ImmutablePureComponent { intl, status, account, + otherAccounts, settings, collapsed, muted, @@ -450,6 +458,7 @@ export default class Status extends ImmutablePureComponent { onOpenMedia, notification, hidden, + unread, featured, ...other } = this.props; @@ -617,6 +626,7 @@ export default class Status extends ImmutablePureComponent { collapsed: isCollapsed, 'has-background': isCollapsed && background, 'status__wrapper-reply': !!status.get('in_reply_to_id'), + read: unread === false, muted, }, 'focusable'); @@ -647,6 +657,7 @@ export default class Status extends ImmutablePureComponent { friend={account} collapsed={isCollapsed} parseClick={parseClick} + otherAccounts={otherAccounts} /> ) : null} </span> @@ -656,6 +667,7 @@ export default class Status extends ImmutablePureComponent { collapsible={settings.getIn(['collapsed', 'enabled'])} collapsed={isCollapsed} setCollapsed={setCollapsed} + directMessage={!!otherAccounts} /> </header> <StatusContent @@ -673,6 +685,7 @@ export default class Status extends ImmutablePureComponent { status={status} account={status.get('account')} showReplyCount={settings.get('show_reply_count')} + directMessage={!!otherAccounts} /> ) : null} {notification ? ( diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index 4c398fd19..85bc4a976 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -71,6 +71,7 @@ export default class StatusActionBar extends ImmutablePureComponent { onBookmark: PropTypes.func, withDismiss: PropTypes.bool, showReplyCount: PropTypes.bool, + directMessage: PropTypes.bool, intl: PropTypes.object.isRequired, }; @@ -191,7 +192,7 @@ export default class StatusActionBar extends ImmutablePureComponent { } render () { - const { status, intl, withDismiss, showReplyCount } = this.props; + const { status, intl, withDismiss, showReplyCount, directMessage } = this.props; const mutingConversation = status.get('muted'); const anonymousAccess = !me; @@ -282,14 +283,15 @@ export default class StatusActionBar extends ImmutablePureComponent { return ( <div className='status__action-bar'> {replyButton} - <IconButton className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(reblogMessage)} icon={reblogIcon} onClick={this.handleReblogClick} /> - <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> - {shareButton} - <IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /> - - <div className='status__action-bar-dropdown'> - <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} /> - </div> + {!directMessage && [ + <IconButton className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(reblogMessage)} icon={reblogIcon} onClick={this.handleReblogClick} />, + <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />, + shareButton, + <IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />, + <div className='status__action-bar-dropdown'> + <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} /> + </div>, + ]} <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> </div> diff --git a/app/javascript/flavours/glitch/components/status_header.js b/app/javascript/flavours/glitch/components/status_header.js index f9321904c..23cff286a 100644 --- a/app/javascript/flavours/glitch/components/status_header.js +++ b/app/javascript/flavours/glitch/components/status_header.js @@ -6,6 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; // Mastodon imports. import Avatar from './avatar'; import AvatarOverlay from './avatar_overlay'; +import AvatarComposite from './avatar_composite'; import DisplayName from './display_name'; export default class StatusHeader extends React.PureComponent { @@ -14,12 +15,18 @@ export default class StatusHeader extends React.PureComponent { status: ImmutablePropTypes.map.isRequired, friend: ImmutablePropTypes.map, parseClick: PropTypes.func.isRequired, + otherAccounts: ImmutablePropTypes.list, }; // Handles clicks on account name/image + handleClick = (id, e) => { + const { parseClick } = this.props; + parseClick(e, `/accounts/${id}`); + } + handleAccountClick = (e) => { - const { status, parseClick } = this.props; - parseClick(e, `/accounts/${status.getIn(['account', 'id'])}`); + const { status } = this.props; + this.handleClick(status.getIn(['account', 'id']), e); } // Rendering. @@ -27,36 +34,55 @@ export default class StatusHeader extends React.PureComponent { const { status, friend, + otherAccounts, } = this.props; const account = status.get('account'); - return ( - <div className='status__info__account' > - <a - href={account.get('url')} - target='_blank' - className='status__avatar' - onClick={this.handleAccountClick} - > - { - friend ? ( - <AvatarOverlay account={account} friend={friend} /> - ) : ( - <Avatar account={account} size={48} /> - ) - } - </a> - <a - href={account.get('url')} - target='_blank' - className='status__display-name' - onClick={this.handleAccountClick} - > - <DisplayName account={account} /> - </a> - </div> - ); + let statusAvatar; + if (otherAccounts && otherAccounts.size > 0) { + statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} onAccountClick={this.handleClick} />; + } else if (friend === undefined || friend === null) { + statusAvatar = <Avatar account={account} size={48} />; + } else { + statusAvatar = <AvatarOverlay account={account} friend={friend} />; + } + + if (!otherAccounts) { + return ( + <div className='status__info__account'> + <a + href={account.get('url')} + target='_blank' + className='status__avatar' + onClick={this.handleAccountClick} + > + {statusAvatar} + </a> + <a + href={account.get('url')} + target='_blank' + className='status__display-name' + onClick={this.handleAccountClick} + > + <DisplayName account={account} others={otherAccounts} /> + </a> + </div> + ); + } else { + // This is a DM conversation + return ( + <div className='status__info__account'> + <span className='status__avatar'> + {statusAvatar} + </span> + + <span className='status__display-name'> + <DisplayName account={account} others={otherAccounts} onAccountClick={this.handleClick} /> + </span> + </div> + ); + } } } diff --git a/app/javascript/flavours/glitch/components/status_icons.js b/app/javascript/flavours/glitch/components/status_icons.js index c9747650f..4a2c62881 100644 --- a/app/javascript/flavours/glitch/components/status_icons.js +++ b/app/javascript/flavours/glitch/components/status_icons.js @@ -22,6 +22,7 @@ export default class StatusIcons extends React.PureComponent { mediaIcon: PropTypes.string, collapsible: PropTypes.bool, collapsed: PropTypes.bool, + directMessage: PropTypes.bool, setCollapsed: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; @@ -42,6 +43,7 @@ export default class StatusIcons extends React.PureComponent { mediaIcon, collapsible, collapsed, + directMessage, intl, } = this.props; @@ -59,9 +61,7 @@ export default class StatusIcons extends React.PureComponent { aria-hidden='true' /> ) : null} - {( - <VisibilityIcon visibility={status.get('visibility')} /> - )} + {!directMessage && <VisibilityIcon visibility={status.get('visibility')} />} {collapsible ? ( <IconButton className='status__collapse-button' |