diff options
Diffstat (limited to 'app/javascript')
27 files changed, 310 insertions, 635 deletions
diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js index 16f13afa4..50bfacc6a 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.js +++ b/app/javascript/flavours/glitch/components/scrollable_list.js @@ -144,7 +144,7 @@ class ScrollableList extends PureComponent { this.attachIntersectionObserver(); attachFullscreenListener(this.onFullScreenChange); - // Handle initial scroll posiiton + // Handle initial scroll position this.handleScroll(); } diff --git a/app/javascript/flavours/glitch/containers/media_container.js b/app/javascript/flavours/glitch/containers/media_container.js index 1ddbc706b..11c15d7c6 100644 --- a/app/javascript/flavours/glitch/containers/media_container.js +++ b/app/javascript/flavours/glitch/containers/media_container.js @@ -43,7 +43,7 @@ export default class MediaContainer extends PureComponent { handleOpenVideo = (options) => { const { components } = this.props; - const { media } = JSON.parse(components[options.componetIndex].getAttribute('data-props')); + const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props')); const mediaList = fromJS(media); document.body.classList.add('with-modals--active'); @@ -87,7 +87,7 @@ export default class MediaContainer extends PureComponent { ...(hashtag ? { hashtag: fromJS(hashtag) } : {}), ...(componentName === 'Video' ? { - componetIndex: i, + componentIndex: i, onOpenVideo: this.handleOpenVideo, } : { onOpenMedia: this.handleOpenMedia, diff --git a/app/javascript/flavours/glitch/features/directory/components/account_card.js b/app/javascript/flavours/glitch/features/directory/components/account_card.js index 2a3fd1ecf..c9ef5850c 100644 --- a/app/javascript/flavours/glitch/features/directory/components/account_card.js +++ b/app/javascript/flavours/glitch/features/directory/components/account_card.js @@ -7,31 +7,28 @@ import { makeGetAccount } from 'flavours/glitch/selectors'; import Avatar from 'flavours/glitch/components/avatar'; import DisplayName from 'flavours/glitch/components/display_name'; import Permalink from 'flavours/glitch/components/permalink'; -import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; -import IconButton from 'flavours/glitch/components/icon_button'; +import Button from 'flavours/glitch/components/button'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { autoPlayGif, me, unfollowModal } from 'flavours/glitch/util/initial_state'; import ShortNumber from 'flavours/glitch/components/short_number'; import { followAccount, unfollowAccount, - blockAccount, unblockAccount, unmuteAccount, } from 'flavours/glitch/actions/accounts'; import { openModal } from 'flavours/glitch/actions/modal'; -import { initMuteModal } from 'flavours/glitch/actions/mutes'; +import classNames from 'classnames'; const messages = defineMessages({ - follow: { id: 'account.follow', defaultMessage: 'Follow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - unfollowConfirm: { - id: 'confirmations.unfollow.confirm', - defaultMessage: 'Unfollow', - }, + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, + unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' }, + unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' }, + unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, + edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, }); const makeMapStateToProps = () => { @@ -75,18 +72,15 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onBlock(account) { if (account.getIn(['relationship', 'blocking'])) { dispatch(unblockAccount(account.get('id'))); - } else { - dispatch(blockAccount(account.get('id'))); } }, onMute(account) { if (account.getIn(['relationship', 'muting'])) { dispatch(unmuteAccount(account.get('id'))); - } else { - dispatch(initMuteModal(account)); } }, + }); export default @@ -138,130 +132,92 @@ class AccountCard extends ImmutablePureComponent { handleMute = () => { this.props.onMute(this.props.account); - }; + } + + handleEditProfile = () => { + window.open('/settings/profile', '_blank'); + } render() { const { account, intl } = this.props; - let buttons; - - if ( - account.get('id') !== me && - account.get('relationship', null) !== null - ) { - const following = account.getIn(['relationship', 'following']); - const requested = account.getIn(['relationship', 'requested']); - const blocking = account.getIn(['relationship', 'blocking']); - const muting = account.getIn(['relationship', 'muting']); - - if (requested) { - buttons = ( - <IconButton - disabled - icon='hourglass' - title={intl.formatMessage(messages.requested)} - /> - ); - } else if (blocking) { - buttons = ( - <IconButton - active - icon='unlock' - title={intl.formatMessage(messages.unblock, { - name: account.get('username'), - })} - onClick={this.handleBlock} - /> - ); - } else if (muting) { - buttons = ( - <IconButton - active - icon='volume-up' - title={intl.formatMessage(messages.unmute, { - name: account.get('username'), - })} - onClick={this.handleMute} - /> - ); - } else if (!account.get('moved') || following) { - buttons = ( - <IconButton - icon={following ? 'user-times' : 'user-plus'} - title={intl.formatMessage( - following ? messages.unfollow : messages.follow, - )} - onClick={this.handleFollow} - active={following} - /> - ); + let actionBtn; + + if (me !== account.get('id')) { + if (!account.get('relationship')) { // Wait until the relationship is loaded + actionBtn = ''; + } else if (account.getIn(['relationship', 'requested'])) { + actionBtn = <Button className={classNames('logo-button')} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />; + } else if (account.getIn(['relationship', 'muting'])) { + actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />; + } else if (!account.getIn(['relationship', 'blocking'])) { + actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />; + } else if (account.getIn(['relationship', 'blocking'])) { + actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />; } + } else { + actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.handleEditProfile} />; } return ( - <div className='directory__card'> - <div className='directory__card__img'> - <img - src={ - autoPlayGif ? account.get('header') : account.get('header_static') - } - alt='' - /> - </div> + <div className='account-card'> + <Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='account-card__permalink'> + <div className='account-card__header'> + <img + src={ + autoPlayGif ? account.get('header') : account.get('header_static') + } + alt='' + /> + </div> - <div className='directory__card__bar'> - <Permalink - className='directory__card__bar__name' - href={account.get('url')} - to={`/@${account.get('acct')}`} - > - <Avatar account={account} size={48} /> + <div className='account-card__title'> + <div className='account-card__title__avatar'><Avatar account={account} size={56} /></div> <DisplayName account={account} /> - </Permalink> - - <div className='directory__card__bar__relationship account__relationship'> - {buttons} </div> - </div> + </Permalink> - <div className='directory__card__extra' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> + {account.get('note').length > 0 && ( <div - className='account__header__content translate' + className='account-card__bio translate' + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} /> - </div> - - <div className='directory__card__extra'> - <div className='accounts-table__count'> - <ShortNumber value={account.get('statuses_count')} /> - <small> - <FormattedMessage id='account.posts' defaultMessage='Toots' /> - </small> + )} + + <div className='account-card__actions'> + <div className='account-card__counters'> + <div className='account-card__counters__item'> + <ShortNumber value={account.get('statuses_count')} /> + <small> + <FormattedMessage id='account.posts' defaultMessage='Toots' /> + </small> + </div> + + <div className='account-card__counters__item'> + {account.get('followers_count') < 0 ? '-' : <ShortNumber value={account.get('followers_count')} />}{' '} + <small> + <FormattedMessage + id='account.followers' + defaultMessage='Followers' + /> + </small> + </div> + + <div className='account-card__counters__item'> + <ShortNumber value={account.get('following_count')} />{' '} + <small> + <FormattedMessage + id='account.following' + defaultMessage='Following' + /> + </small> + </div> </div> - <div className='accounts-table__count'> - {account.get('followers_count') < 0 ? '-' : <ShortNumber value={account.get('followers_count')} />}{' '} - <small> - <FormattedMessage - id='account.followers' - defaultMessage='Followers' - /> - </small> - </div> - <div className='accounts-table__count'> - {account.get('last_status_at') === null ? ( - <FormattedMessage - id='account.never_active' - defaultMessage='Never' - /> - ) : ( - <RelativeTimestamp timestamp={account.get('last_status_at')} /> - )}{' '} - <small> - <FormattedMessage - id='account.last_status' - defaultMessage='Last active' - /> - </small> + + <div className='account-card__actions__button'> + {actionBtn} </div> </div> </div> diff --git a/app/javascript/flavours/glitch/features/directory/index.js b/app/javascript/flavours/glitch/features/directory/index.js index cde5926e0..87d9b3625 100644 --- a/app/javascript/flavours/glitch/features/directory/index.js +++ b/app/javascript/flavours/glitch/features/directory/index.js @@ -10,9 +10,9 @@ import { fetchDirectory, expandDirectory } from 'flavours/glitch/actions/directo import { List as ImmutableList } from 'immutable'; import AccountCard from './components/account_card'; import RadioButton from 'flavours/glitch/components/radio_button'; -import classNames from 'classnames'; import LoadMore from 'flavours/glitch/components/load_more'; import ScrollContainer from 'flavours/glitch/containers/scroll_container'; +import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; const messages = defineMessages({ title: { id: 'column.directory', defaultMessage: 'Browse profiles' }, @@ -129,7 +129,7 @@ class Directory extends React.PureComponent { const pinned = !!columnId; const scrollableArea = ( - <div className='scrollable' style={{ background: 'transparent' }}> + <div className='scrollable'> <div className='filter-form'> <div className='filter-form__column' role='group'> <RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} /> @@ -142,8 +142,10 @@ class Directory extends React.PureComponent { </div> </div> - <div className={classNames('directory__list', { loading: isLoading })}> - {accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)} + <div className='directory__list'> + {isLoading ? <LoadingIndicator /> : accountIds.map(accountId => ( + <AccountCard id={accountId} key={accountId} /> + ))} </div> <LoadMore onClick={this.handleLoadMore} visible={!isLoading} /> diff --git a/app/javascript/flavours/glitch/features/report/category.js b/app/javascript/flavours/glitch/features/report/category.js index ddbc82563..cf63533d0 100644 --- a/app/javascript/flavours/glitch/features/report/category.js +++ b/app/javascript/flavours/glitch/features/report/category.js @@ -8,7 +8,7 @@ const messages = defineMessages({ dislike: { id: 'report.reasons.dislike', defaultMessage: 'I don\'t like it' }, dislike_description: { id: 'report.reasons.dislike_description', defaultMessage: 'It is not something you want to see' }, spam: { id: 'report.reasons.spam', defaultMessage: 'It\'s spam' }, - spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetetive replies' }, + spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetitive replies' }, violation: { id: 'report.reasons.violation', defaultMessage: 'It violates server rules' }, violation_description: { id: 'report.reasons.violation_description', defaultMessage: 'You are aware that it breaks specific rules' }, other: { id: 'report.reasons.other', defaultMessage: 'It\'s something else' }, diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index fcbf07ce2..53e3dfda3 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -123,7 +123,7 @@ class Video extends React.PureComponent { autoPlay: PropTypes.bool, volume: PropTypes.number, muted: PropTypes.bool, - componetIndex: PropTypes.number, + componentIndex: PropTypes.number, }; static defaultProps = { @@ -516,7 +516,7 @@ class Video extends React.PureComponent { startTime: this.video.currentTime, autoPlay: !this.state.paused, defaultVolume: this.state.volume, - componetIndex: this.props.componetIndex, + componentIndex: this.props.componentIndex, }); } diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 73414785c..0873ac300 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -1236,6 +1236,11 @@ a.sparkline { background: $ui-base-color; border-radius: 4px; + &__permalink { + color: inherit; + text-decoration: none; + } + &__header { padding: 4px; border-radius: 4px; @@ -1252,20 +1257,22 @@ a.sparkline { } &__title { - margin-top: -25px; + margin-top: -(15px + 8px); display: flex; align-items: flex-end; &__avatar { - padding: 15px; + padding: 14px; - img { + img, + .account__avatar { display: block; margin: 0; width: 56px; height: 56px; - background: darken($ui-base-color, 8%); + background-color: darken($ui-base-color, 8%); border-radius: 8px; + border: 1px solid $ui-base-color; } } @@ -1273,30 +1280,34 @@ a.sparkline { color: $darker-text-color; padding-bottom: 15px; font-size: 15px; + line-height: 20px; bdi { display: block; color: $primary-text-color; - font-weight: 500; + font-weight: 700; } } } &__bio { padding: 0 15px; + margin: 8px 0; overflow: hidden; text-overflow: ellipsis; word-wrap: break-word; - max-height: 18px * 2; + max-height: 21px * 2; position: relative; + font-size: 15px; + line-height: 21px; &::after { display: block; content: ""; width: 50px; - height: 18px; + height: 21px; position: absolute; - bottom: 0; + bottom: 8px; right: 15px; background: linear-gradient(to left, $ui-base-color, transparent); pointer-events: none; @@ -1309,10 +1320,6 @@ a.sparkline { &:hover { text-decoration: underline; - - .fa { - color: lighten($dark-text-color, 7%); - } } &.mention { @@ -1329,12 +1336,21 @@ a.sparkline { &__actions { display: flex; + justify-content: space-between; align-items: center; - padding-top: 10px; &__button { - flex: 0 0 auto; + flex-shrink: 1; padding: 0 15px; + overflow: hidden; + + .button { + min-width: 0; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + max-width: 100%; + } } } @@ -1343,19 +1359,23 @@ a.sparkline { display: grid; grid-auto-columns: minmax(0, 1fr); grid-auto-flow: column; + max-width: 340px; + min-width: 65px * 3; &__item { - padding: 15px; + padding: 15px 0; text-align: center; color: $primary-text-color; font-weight: 600; font-size: 15px; + line-height: 21px; small { display: block; color: $darker-text-color; font-weight: 400; font-size: 13px; + line-height: 18px; } } } diff --git a/app/javascript/flavours/glitch/styles/components/directory.scss b/app/javascript/flavours/glitch/styles/components/directory.scss index b0ad5a88a..b48c6c102 100644 --- a/app/javascript/flavours/glitch/styles/components/directory.scss +++ b/app/javascript/flavours/glitch/styles/components/directory.scss @@ -1,133 +1,17 @@ -.directory { - &__list { - width: 100%; - margin: 10px 0; - transition: opacity 100ms ease-in; - - &.loading { - opacity: 0.7; - } +.scrollable .account-card { + margin: 10px; + background: lighten($ui-base-color, 8%); +} - @media screen and (max-width: $no-gap-breakpoint) { - margin: 0; - } +.scrollable .account-card__title__avatar { + img, + .account__avatar { + border-color: lighten($ui-base-color, 8%); } +} - &__card { - box-sizing: border-box; - margin-bottom: 10px; - - &__img { - height: 125px; - position: relative; - background: darken($ui-base-color, 12%); - overflow: hidden; - - img { - display: block; - width: 100%; - height: 100%; - margin: 0; - object-fit: cover; - } - } - - &__bar { - display: flex; - align-items: center; - background: lighten($ui-base-color, 4%); - padding: 10px; - - &__name { - flex: 1 1 auto; - display: flex; - align-items: center; - text-decoration: none; - overflow: hidden; - } - - &__relationship { - width: 23px; - min-height: 1px; - flex: 0 0 auto; - } - - .avatar { - flex: 0 0 auto; - width: 48px; - height: 48px; - padding-top: 2px; - - img { - width: 100%; - height: 100%; - display: block; - margin: 0; - border-radius: 4px; - background: darken($ui-base-color, 8%); - object-fit: cover; - } - } - - .display-name { - margin-left: 15px; - text-align: left; - - strong { - font-size: 15px; - color: $primary-text-color; - font-weight: 500; - overflow: hidden; - text-overflow: ellipsis; - } - - span { - display: block; - font-size: 14px; - color: $darker-text-color; - font-weight: 400; - overflow: hidden; - text-overflow: ellipsis; - } - } - } - - &__extra { - background: $ui-base-color; - display: flex; - align-items: center; - justify-content: center; - - .accounts-table__count { - width: 33.33%; - flex: 0 0 auto; - padding: 15px 0; - } - - .account__header__content { - box-sizing: border-box; - padding: 15px 10px; - border-bottom: 1px solid lighten($ui-base-color, 8%); - width: 100%; - min-height: 18px + 30px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - p { - display: none; - - &:first-child { - display: inline; - } - } - - br { - display: none; - } - } - } - } +.scrollable .account-card__bio::after { + background: linear-gradient(to left, lighten($ui-base-color, 8%), transparent); } .filter-form { @@ -135,6 +19,7 @@ &__column { padding: 10px 15px; + padding-bottom: 0; } .radio-button { diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index b6372c096..7364eba91 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -41,7 +41,7 @@ cursor: pointer; display: inline-block; font-family: inherit; - font-size: 17px; + font-size: 15px; font-weight: 500; letter-spacing: 0; line-height: 22px; diff --git a/app/javascript/flavours/glitch/styles/components/single_column.scss b/app/javascript/flavours/glitch/styles/components/single_column.scss index edf705b5f..db510f1f4 100644 --- a/app/javascript/flavours/glitch/styles/components/single_column.scss +++ b/app/javascript/flavours/glitch/styles/components/single_column.scss @@ -94,17 +94,7 @@ padding: 0; } - .directory__list { - display: grid; - grid-gap: 10px; - grid-template-columns: minmax(0, 50%) minmax(0, 50%); - - @media screen and (max-width: $no-gap-breakpoint) { - display: block; - } - } - - .directory__card { + .account-card { margin-bottom: 0; } diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss index eb82157c8..98a1288eb 100644 --- a/app/javascript/flavours/glitch/styles/containers.scss +++ b/app/javascript/flavours/glitch/styles/containers.scss @@ -411,14 +411,6 @@ } } - .directory__card { - border-radius: 4px; - - @media screen and (max-width: $no-gap-breakpoint) { - border-radius: 0; - } - } - .page-header { @media screen and (max-width: $no-gap-breakpoint) { border-bottom: 0; @@ -841,19 +833,21 @@ grid-gap: 10px; grid-template-columns: minmax(0, 50%) minmax(0, 50%); + .account-card { + display: flex; + flex-direction: column; + } + @media screen and (max-width: $no-gap-breakpoint) { display: block; - } - .icon-button { - font-size: 18px; + .account-card { + margin-bottom: 10px; + display: block; + } } } - .directory__card { - margin-bottom: 0; - } - .card-grid { display: flex; flex-wrap: wrap; diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss index a2cdecf06..0847c8f4c 100644 --- a/app/javascript/flavours/glitch/styles/polls.scss +++ b/app/javascript/flavours/glitch/styles/polls.scss @@ -75,7 +75,7 @@ display: none; } - .autossugest-input { + .autosuggest-input { flex: 1 1 auto; } diff --git a/app/javascript/flavours/glitch/styles/rtl.scss b/app/javascript/flavours/glitch/styles/rtl.scss index afa05d93e..d0153c9f9 100644 --- a/app/javascript/flavours/glitch/styles/rtl.scss +++ b/app/javascript/flavours/glitch/styles/rtl.scss @@ -12,11 +12,6 @@ body.rtl { margin-left: 10px; } - .directory__card__bar .display-name { - margin-left: 0; - margin-right: 15px; - } - .display-name { text-align: right; } diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index 68a178512..91d04bf4d 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -151,7 +151,7 @@ class ScrollableList extends PureComponent { attachFullscreenListener(this.onFullScreenChange); - // Handle initial scroll posiiton + // Handle initial scroll position this.handleScroll(); } diff --git a/app/javascript/mastodon/containers/media_container.js b/app/javascript/mastodon/containers/media_container.js index 2f42a084f..6ee1f0bd8 100644 --- a/app/javascript/mastodon/containers/media_container.js +++ b/app/javascript/mastodon/containers/media_container.js @@ -43,7 +43,7 @@ export default class MediaContainer extends PureComponent { handleOpenVideo = (options) => { const { components } = this.props; - const { media } = JSON.parse(components[options.componetIndex].getAttribute('data-props')); + const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props')); const mediaList = fromJS(media); document.body.classList.add('with-modals--active'); @@ -87,7 +87,7 @@ export default class MediaContainer extends PureComponent { ...(hashtag ? { hashtag: fromJS(hashtag) } : {}), ...(componentName === 'Video' ? { - componetIndex: i, + componentIndex: i, onOpenVideo: this.handleOpenVideo, } : { onOpenMedia: this.handleOpenMedia, diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js index 03e13f28e..31f59cd84 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.js +++ b/app/javascript/mastodon/features/directory/components/account_card.js @@ -7,31 +7,28 @@ import { makeGetAccount } from 'mastodon/selectors'; import Avatar from 'mastodon/components/avatar'; import DisplayName from 'mastodon/components/display_name'; import Permalink from 'mastodon/components/permalink'; -import RelativeTimestamp from 'mastodon/components/relative_timestamp'; -import IconButton from 'mastodon/components/icon_button'; +import Button from 'mastodon/components/button'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state'; import ShortNumber from 'mastodon/components/short_number'; import { followAccount, unfollowAccount, - blockAccount, unblockAccount, unmuteAccount, } from 'mastodon/actions/accounts'; import { openModal } from 'mastodon/actions/modal'; -import { initMuteModal } from 'mastodon/actions/mutes'; +import classNames from 'classnames'; const messages = defineMessages({ - follow: { id: 'account.follow', defaultMessage: 'Follow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - unfollowConfirm: { - id: 'confirmations.unfollow.confirm', - defaultMessage: 'Unfollow', - }, + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, + unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' }, + unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' }, + unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, + edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, }); const makeMapStateToProps = () => { @@ -75,18 +72,15 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onBlock(account) { if (account.getIn(['relationship', 'blocking'])) { dispatch(unblockAccount(account.get('id'))); - } else { - dispatch(blockAccount(account.get('id'))); } }, onMute(account) { if (account.getIn(['relationship', 'muting'])) { dispatch(unmuteAccount(account.get('id'))); - } else { - dispatch(initMuteModal(account)); } }, + }); export default @@ -138,130 +132,92 @@ class AccountCard extends ImmutablePureComponent { handleMute = () => { this.props.onMute(this.props.account); - }; + } + + handleEditProfile = () => { + window.open('/settings/profile', '_blank'); + } render() { const { account, intl } = this.props; - let buttons; - - if ( - account.get('id') !== me && - account.get('relationship', null) !== null - ) { - const following = account.getIn(['relationship', 'following']); - const requested = account.getIn(['relationship', 'requested']); - const blocking = account.getIn(['relationship', 'blocking']); - const muting = account.getIn(['relationship', 'muting']); - - if (requested) { - buttons = ( - <IconButton - disabled - icon='hourglass' - title={intl.formatMessage(messages.requested)} - /> - ); - } else if (blocking) { - buttons = ( - <IconButton - active - icon='unlock' - title={intl.formatMessage(messages.unblock, { - name: account.get('username'), - })} - onClick={this.handleBlock} - /> - ); - } else if (muting) { - buttons = ( - <IconButton - active - icon='volume-up' - title={intl.formatMessage(messages.unmute, { - name: account.get('username'), - })} - onClick={this.handleMute} - /> - ); - } else if (!account.get('moved') || following) { - buttons = ( - <IconButton - icon={following ? 'user-times' : 'user-plus'} - title={intl.formatMessage( - following ? messages.unfollow : messages.follow, - )} - onClick={this.handleFollow} - active={following} - /> - ); + let actionBtn; + + if (me !== account.get('id')) { + if (!account.get('relationship')) { // Wait until the relationship is loaded + actionBtn = ''; + } else if (account.getIn(['relationship', 'requested'])) { + actionBtn = <Button className={classNames('logo-button')} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />; + } else if (account.getIn(['relationship', 'muting'])) { + actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />; + } else if (!account.getIn(['relationship', 'blocking'])) { + actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />; + } else if (account.getIn(['relationship', 'blocking'])) { + actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />; } + } else { + actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.handleEditProfile} />; } return ( - <div className='directory__card'> - <div className='directory__card__img'> - <img - src={ - autoPlayGif ? account.get('header') : account.get('header_static') - } - alt='' - /> - </div> + <div className='account-card'> + <Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='account-card__permalink'> + <div className='account-card__header'> + <img + src={ + autoPlayGif ? account.get('header') : account.get('header_static') + } + alt='' + /> + </div> - <div className='directory__card__bar'> - <Permalink - className='directory__card__bar__name' - href={account.get('url')} - to={`/@${account.get('acct')}`} - > - <Avatar account={account} size={48} /> + <div className='account-card__title'> + <div className='account-card__title__avatar'><Avatar account={account} size={56} /></div> <DisplayName account={account} /> - </Permalink> - - <div className='directory__card__bar__relationship account__relationship'> - {buttons} </div> - </div> + </Permalink> - <div className='directory__card__extra' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> + {account.get('note').length > 0 && ( <div - className='account__header__content translate' + className='account-card__bio translate' + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} /> - </div> - - <div className='directory__card__extra'> - <div className='accounts-table__count'> - <ShortNumber value={account.get('statuses_count')} /> - <small> - <FormattedMessage id='account.posts' defaultMessage='Toots' /> - </small> + )} + + <div className='account-card__actions'> + <div className='account-card__counters'> + <div className='account-card__counters__item'> + <ShortNumber value={account.get('statuses_count')} /> + <small> + <FormattedMessage id='account.posts' defaultMessage='Toots' /> + </small> + </div> + + <div className='account-card__counters__item'> + <ShortNumber value={account.get('followers_count')} />{' '} + <small> + <FormattedMessage + id='account.followers' + defaultMessage='Followers' + /> + </small> + </div> + + <div className='account-card__counters__item'> + <ShortNumber value={account.get('following_count')} />{' '} + <small> + <FormattedMessage + id='account.following' + defaultMessage='Following' + /> + </small> + </div> </div> - <div className='accounts-table__count'> - <ShortNumber value={account.get('followers_count')} />{' '} - <small> - <FormattedMessage - id='account.followers' - defaultMessage='Followers' - /> - </small> - </div> - <div className='accounts-table__count'> - {account.get('last_status_at') === null ? ( - <FormattedMessage - id='account.never_active' - defaultMessage='Never' - /> - ) : ( - <RelativeTimestamp timestamp={account.get('last_status_at')} /> - )}{' '} - <small> - <FormattedMessage - id='account.last_status' - defaultMessage='Last active' - /> - </small> + + <div className='account-card__actions__button'> + {actionBtn} </div> </div> </div> diff --git a/app/javascript/mastodon/features/directory/index.js b/app/javascript/mastodon/features/directory/index.js index 88f20d330..94d7d1a9c 100644 --- a/app/javascript/mastodon/features/directory/index.js +++ b/app/javascript/mastodon/features/directory/index.js @@ -10,9 +10,9 @@ import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory'; import { List as ImmutableList } from 'immutable'; import AccountCard from './components/account_card'; import RadioButton from 'mastodon/components/radio_button'; -import classNames from 'classnames'; import LoadMore from 'mastodon/components/load_more'; import ScrollContainer from 'mastodon/containers/scroll_container'; +import LoadingIndicator from 'mastodon/components/loading_indicator'; const messages = defineMessages({ title: { id: 'column.directory', defaultMessage: 'Browse profiles' }, @@ -129,7 +129,7 @@ class Directory extends React.PureComponent { const pinned = !!columnId; const scrollableArea = ( - <div className='scrollable' style={{ background: 'transparent' }}> + <div className='scrollable'> <div className='filter-form'> <div className='filter-form__column' role='group'> <RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} /> @@ -142,8 +142,10 @@ class Directory extends React.PureComponent { </div> </div> - <div className={classNames('directory__list', { loading: isLoading })}> - {accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)} + <div className='directory__list'> + {isLoading ? <LoadingIndicator /> : accountIds.map(accountId => ( + <AccountCard id={accountId} key={accountId} /> + ))} </div> <LoadMore onClick={this.handleLoadMore} visible={!isLoading} /> diff --git a/app/javascript/mastodon/features/explore/suggestions.js b/app/javascript/mastodon/features/explore/suggestions.js index c094a8d93..0c6a7ef8a 100644 --- a/app/javascript/mastodon/features/explore/suggestions.js +++ b/app/javascript/mastodon/features/explore/suggestions.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import Account from 'mastodon/containers/account_container'; +import AccountCard from 'mastodon/features/directory/components/account_card'; import LoadingIndicator from 'mastodon/components/loading_indicator'; import { connect } from 'react-redux'; import { fetchSuggestions } from 'mastodon/actions/suggestions'; @@ -29,9 +29,9 @@ class Suggestions extends React.PureComponent { const { isLoading, suggestions } = this.props; return ( - <div className='explore__links'> - {isLoading ? (<LoadingIndicator />) : suggestions.map(suggestion => ( - <Account key={suggestion.get('account')} id={suggestion.get('account')} /> + <div className='explore__suggestions'> + {isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => ( + <AccountCard key={suggestion.get('account')} id={suggestion.get('account')} /> ))} </div> ); diff --git a/app/javascript/mastodon/features/report/category.js b/app/javascript/mastodon/features/report/category.js index 122b51c7c..a36dc81b1 100644 --- a/app/javascript/mastodon/features/report/category.js +++ b/app/javascript/mastodon/features/report/category.js @@ -8,7 +8,7 @@ const messages = defineMessages({ dislike: { id: 'report.reasons.dislike', defaultMessage: 'I don\'t like it' }, dislike_description: { id: 'report.reasons.dislike_description', defaultMessage: 'It is not something you want to see' }, spam: { id: 'report.reasons.spam', defaultMessage: 'It\'s spam' }, - spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetetive replies' }, + spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetitive replies' }, violation: { id: 'report.reasons.violation', defaultMessage: 'It violates server rules' }, violation_description: { id: 'report.reasons.violation_description', defaultMessage: 'You are aware that it breaks specific rules' }, other: { id: 'report.reasons.other', defaultMessage: 'It\'s something else' }, diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 70e3cd6e8..8d47e479a 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -121,7 +121,7 @@ class Video extends React.PureComponent { autoPlay: PropTypes.bool, volume: PropTypes.number, muted: PropTypes.bool, - componetIndex: PropTypes.number, + componentIndex: PropTypes.number, }; static defaultProps = { @@ -502,7 +502,7 @@ class Video extends React.PureComponent { startTime: this.video.currentTime, autoPlay: !this.state.paused, defaultVolume: this.state.volume, - componetIndex: this.props.componetIndex, + componentIndex: this.props.componentIndex, }); } diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 89fd20c3f..06c2b679c 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -414,7 +414,7 @@ "report.reasons.other": "It's something else", "report.reasons.other_description": "The issue does not fit into other categories", "report.reasons.spam": "It's spam", - "report.reasons.spam_description": "Malicious links, fake engagement, or repetetive replies", + "report.reasons.spam_description": "Malicious links, fake engagement, or repetitive replies", "report.reasons.violation": "It violates server rules", "report.reasons.violation_description": "You are aware that it breaks specific rules", "report.rules.subtitle": "Select all that apply", diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index 8e6b0cdd5..eb6bdea99 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -40,19 +40,11 @@ html { background: lighten($ui-base-color, 12%); } -.filter-form, -.directory__card__bar { +.filter-form { background: $white; border-bottom: 1px solid lighten($ui-base-color, 8%); } -.scrollable .directory__list { - width: calc(100% + 2px); - margin-left: -1px; - margin-right: -1px; -} - -.directory__card, .table-of-contents { border: 1px solid lighten($ui-base-color, 8%); } @@ -75,8 +67,7 @@ html { .column-header__back-button, .column-header__button, .column-header__button.active, -.account__header__bar, -.directory__card__extra { +.account__header__bar { background: $white; } diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 73414785c..0873ac300 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -1236,6 +1236,11 @@ a.sparkline { background: $ui-base-color; border-radius: 4px; + &__permalink { + color: inherit; + text-decoration: none; + } + &__header { padding: 4px; border-radius: 4px; @@ -1252,20 +1257,22 @@ a.sparkline { } &__title { - margin-top: -25px; + margin-top: -(15px + 8px); display: flex; align-items: flex-end; &__avatar { - padding: 15px; + padding: 14px; - img { + img, + .account__avatar { display: block; margin: 0; width: 56px; height: 56px; - background: darken($ui-base-color, 8%); + background-color: darken($ui-base-color, 8%); border-radius: 8px; + border: 1px solid $ui-base-color; } } @@ -1273,30 +1280,34 @@ a.sparkline { color: $darker-text-color; padding-bottom: 15px; font-size: 15px; + line-height: 20px; bdi { display: block; color: $primary-text-color; - font-weight: 500; + font-weight: 700; } } } &__bio { padding: 0 15px; + margin: 8px 0; overflow: hidden; text-overflow: ellipsis; word-wrap: break-word; - max-height: 18px * 2; + max-height: 21px * 2; position: relative; + font-size: 15px; + line-height: 21px; &::after { display: block; content: ""; width: 50px; - height: 18px; + height: 21px; position: absolute; - bottom: 0; + bottom: 8px; right: 15px; background: linear-gradient(to left, $ui-base-color, transparent); pointer-events: none; @@ -1309,10 +1320,6 @@ a.sparkline { &:hover { text-decoration: underline; - - .fa { - color: lighten($dark-text-color, 7%); - } } &.mention { @@ -1329,12 +1336,21 @@ a.sparkline { &__actions { display: flex; + justify-content: space-between; align-items: center; - padding-top: 10px; &__button { - flex: 0 0 auto; + flex-shrink: 1; padding: 0 15px; + overflow: hidden; + + .button { + min-width: 0; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + max-width: 100%; + } } } @@ -1343,19 +1359,23 @@ a.sparkline { display: grid; grid-auto-columns: minmax(0, 1fr); grid-auto-flow: column; + max-width: 340px; + min-width: 65px * 3; &__item { - padding: 15px; + padding: 15px 0; text-align: center; color: $primary-text-color; font-weight: 600; font-size: 15px; + line-height: 21px; small { display: block; color: $darker-text-color; font-weight: 400; font-size: 13px; + line-height: 18px; } } } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 8f6d4b69a..d1d679ac2 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -50,7 +50,7 @@ cursor: pointer; display: inline-block; font-family: inherit; - font-size: 17px; + font-size: 15px; font-weight: 500; letter-spacing: 0; line-height: 22px; @@ -2333,17 +2333,7 @@ a.account__display-name { padding: 0; } - .directory__list { - display: grid; - grid-gap: 10px; - grid-template-columns: minmax(0, 50%) minmax(0, 50%); - - @media screen and (max-width: $no-gap-breakpoint) { - display: block; - } - } - - .directory__card { + .account-card { margin-bottom: 0; } @@ -4315,7 +4305,7 @@ a.status-card.compact:hover { } } -.upload-progess__message { +.upload-progress__message { flex: 1 1 auto; } @@ -6219,136 +6209,20 @@ a.status-card.compact:hover { } } -.directory { - &__list { - width: 100%; - margin: 10px 0; - transition: opacity 100ms ease-in; - - &.loading { - opacity: 0.7; - } +.scrollable .account-card { + margin: 10px; + background: lighten($ui-base-color, 8%); +} - @media screen and (max-width: $no-gap-breakpoint) { - margin: 0; - } +.scrollable .account-card__title__avatar { + img, + .account__avatar { + border-color: lighten($ui-base-color, 8%); } +} - &__card { - box-sizing: border-box; - margin-bottom: 10px; - - &__img { - height: 125px; - position: relative; - background: darken($ui-base-color, 12%); - overflow: hidden; - - img { - display: block; - width: 100%; - height: 100%; - margin: 0; - object-fit: cover; - } - } - - &__bar { - display: flex; - align-items: center; - background: lighten($ui-base-color, 4%); - padding: 10px; - - &__name { - flex: 1 1 auto; - display: flex; - align-items: center; - text-decoration: none; - overflow: hidden; - } - - &__relationship { - width: 23px; - min-height: 1px; - flex: 0 0 auto; - } - - .avatar { - flex: 0 0 auto; - width: 48px; - height: 48px; - padding-top: 2px; - - img { - width: 100%; - height: 100%; - display: block; - margin: 0; - border-radius: 4px; - background: darken($ui-base-color, 8%); - object-fit: cover; - } - } - - .display-name { - margin-left: 15px; - text-align: left; - - strong { - font-size: 15px; - color: $primary-text-color; - font-weight: 500; - overflow: hidden; - text-overflow: ellipsis; - } - - span { - display: block; - font-size: 14px; - color: $darker-text-color; - font-weight: 400; - overflow: hidden; - text-overflow: ellipsis; - } - } - } - - &__extra { - background: $ui-base-color; - display: flex; - align-items: center; - justify-content: center; - - .accounts-table__count { - width: 33.33%; - flex: 0 0 auto; - padding: 15px 0; - } - - .account__header__content { - box-sizing: border-box; - padding: 15px 10px; - border-bottom: 1px solid lighten($ui-base-color, 8%); - width: 100%; - min-height: 18px + 30px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - p { - display: none; - - &:first-child { - display: inline; - } - } - - br { - display: none; - } - } - } - } +.scrollable .account-card__bio::after { + background: linear-gradient(to left, lighten($ui-base-color, 8%), transparent); } .account-gallery__container { @@ -6452,6 +6326,7 @@ a.status-card.compact:hover { &__column { padding: 10px 15px; + padding-bottom: 0; } .radio-button { diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index a180df437..81459f5ba 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -409,14 +409,6 @@ } } - .directory__card { - border-radius: 4px; - - @media screen and (max-width: $no-gap-breakpoint) { - border-radius: 0; - } - } - .page-header { @media screen and (max-width: $no-gap-breakpoint) { border-bottom: 0; @@ -835,19 +827,21 @@ grid-gap: 10px; grid-template-columns: minmax(0, 50%) minmax(0, 50%); + .account-card { + display: flex; + flex-direction: column; + } + @media screen and (max-width: $no-gap-breakpoint) { display: block; - } - .icon-button { - font-size: 18px; + .account-card { + margin-bottom: 10px; + display: block; + } } } - .directory__card { - margin-bottom: 0; - } - .card-grid { display: flex; flex-wrap: wrap; diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss index e33fc7983..a719044ea 100644 --- a/app/javascript/styles/mastodon/polls.scss +++ b/app/javascript/styles/mastodon/polls.scss @@ -69,7 +69,7 @@ display: none; } - .autossugest-input { + .autosuggest-input { flex: 1 1 auto; } diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss index ea7bb5113..98eb1511c 100644 --- a/app/javascript/styles/mastodon/rtl.scss +++ b/app/javascript/styles/mastodon/rtl.scss @@ -12,11 +12,6 @@ body.rtl { margin-left: 10px; } - .directory__card__bar .display-name { - margin-left: 0; - margin-right: 15px; - } - .display-name, .announcements__item { text-align: right; |