Add advanced mode to the account column

This commit is contained in:
noellabo 2022-09-18 05:15:16 +09:00
parent 4b3f4be472
commit 2bee3e3fdb
24 changed files with 1296 additions and 165 deletions

View file

@ -0,0 +1,34 @@
import api from '../api';
export const FEATURED_TAGS_FETCH_REQUEST = 'FEATURED_TAGS_FETCH_REQUEST';
export const FEATURED_TAGS_FETCH_SUCCESS = 'FEATURED_TAGS_FETCH_SUCCESS';
export const FEATURED_TAGS_FETCH_FAIL = 'FEATURED_TAGS_FETCH_FAIL';
export const fetchFeaturedTags = (id) => (dispatch, getState) => {
if (getState().getIn(['user_lists', 'featured_tags', id, 'items'])) {
return;
}
dispatch(fetchFeaturedTagsRequest(id));
api(getState).get(`/api/v1/accounts/${id}/featured_tags`)
.then(({ data }) => dispatch(fetchFeaturedTagsSuccess(id, data)))
.catch(err => dispatch(fetchFeaturedTagsFail(id, err)));
};
export const fetchFeaturedTagsRequest = (id) => ({
type: FEATURED_TAGS_FETCH_REQUEST,
id,
});
export const fetchFeaturedTagsSuccess = (id, tags) => ({
type: FEATURED_TAGS_FETCH_SUCCESS,
id,
tags,
});
export const fetchFeaturedTagsFail = (id, error) => ({
type: FEATURED_TAGS_FETCH_FAIL,
id,
error,
});

View file

@ -182,9 +182,9 @@ export const expandLimitedTimeline = ({ maxId, visibilities } = {}, done
export const expandPublicTimeline = ({ maxId, onlyMedia, withoutMedia, withoutBot, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${withoutBot ? ':nobot' : ':bot'}${withoutMedia ? ':nomedia' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia, without_media: !!withoutMedia, without_bot: !!withoutBot }, done);
export const expandDomainTimeline = (domain, { maxId, onlyMedia, withoutMedia, withoutBot } = {}, done = noOp) => expandTimeline(`domain${withoutBot ? ':nobot' : ':bot'}${withoutMedia ? ':nomedia' : ''}${onlyMedia ? ':media' : ''}:${domain}`, '/api/v1/timelines/public', { local: false, domain: domain, max_id: maxId, only_media: !!onlyMedia, without_media: !!withoutMedia, without_bot: !!withoutBot }, done);
export const expandGroupTimeline = (id, { maxId, onlyMedia, withoutMedia, tagged } = {}, done = noOp) => expandTimeline(`group:${id}${withoutMedia ? ':nomedia' : ''}${onlyMedia ? ':media' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/timelines/group/${id}`, { max_id: maxId, only_media: !!onlyMedia, without_media: !!withoutMedia, tagged: tagged }, done);
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
export const expandAccountTimeline = (accountId, { maxId, withReplies, withoutReblogs, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${withoutReblogs ? ':without_reblogs' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withoutReblogs, tagged, max_id: maxId });
export const expandAccountCoversations = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:conversations`, `/api/v1/accounts/${accountId}/conversations`, { max_id: maxId });
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {

View file

@ -10,6 +10,7 @@ import ScrollableList from './scrollable_list';
import RegenerationIndicator from 'mastodon/components/regeneration_indicator';
import { isIOS } from 'mastodon/is_mobile';
import { showReloadButton } from '../initial_state';
import { List as ImmutableList } from 'immutable';
export default class StatusList extends ImmutablePureComponent {
@ -112,7 +113,7 @@ export default class StatusList extends ImmutablePureComponent {
showCard={showCard}
/>
))
) : null;
) : ImmutableList();
if (scrollableContent && featuredStatusIds) {
scrollableContent = featuredStatusIds.map(statusId => (

View file

@ -0,0 +1,72 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
import Permalink from 'mastodon/components/permalink';
import ShortNumber from 'mastodon/components/short_number';
import { List as ImmutableList } from 'immutable';
const messages = defineMessages({
hashtag_all: { id: 'account.hashtag_all', defaultMessage: 'All' },
hashtag_all_description: { id: 'account.hashtag_all_description', defaultMessage: 'All posts (deselect hashtags)' },
hashtag_select_description: { id: 'account.hashtag_select_description', defaultMessage: 'Select hashtag #{name}' },
statuses_counter: { id: 'account.statuses_counter', defaultMessage: '{count, plural, one {{counter} Post} other {{counter} Posts}}' },
});
const mapStateToProps = (state, { account }) => ({
featuredTags: state.getIn(['user_lists', 'featured_tags', account.get('id'), 'items'], ImmutableList()),
});
export default @connect(mapStateToProps)
@injectIntl
class FeaturedTags extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
account: ImmutablePropTypes.map,
featuredTags: ImmutablePropTypes.list,
tagged: PropTypes.string,
intl: PropTypes.object.isRequired,
};
render () {
const { account, featuredTags, tagged, intl } = this.props;
if (!account || featuredTags.isEmpty()) {
return null;
}
const suspended = account.get('suspended');
const accountId = account.get('id');
return (
<div className={classNames('account__header', 'advanced', { inactive: !!account.get('moved') })}>
<div className='account__header__extra'>
<div className='account__header__extra__hashtag-links'>
<Permalink key='all' className={classNames('account__hashtag-link', { active: !tagged })} title={intl.formatMessage(messages.hashtag_all_description)} href={account.get('url')} to={`/accounts/${accountId}/posts`}>{intl.formatMessage(messages.hashtag_all)}</Permalink>
{!suspended && featuredTags.map(featuredTag => {
const name = featuredTag.get('name');
const url = featuredTag.get('url');
const to = `/accounts/${accountId}/posts/${name}`;
const desc = intl.formatMessage(messages.hashtag_select_description, { name });
const count = featuredTag.get('statuses_count');
return (
<Permalink key={`#${name}`} className={classNames('account__hashtag-link', { active: this.context.router.history.location.pathname === to })} title={desc} href={url} to={to}>
#{name} <span title={intl.formatMessage(messages.statuses_counter, { count: count, counter: intl.formatNumber(count) })}>({<ShortNumber value={count} />})</span>
</Permalink>
);
})}
</div>
</div>
</div>
);
}
}

View file

@ -93,6 +93,7 @@ class Header extends ImmutablePureComponent {
onEditAccountNote: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
domain: PropTypes.string.isRequired,
hideProfile: PropTypes.bool.isRequired,
};
openEditProfile = () => {
@ -154,7 +155,7 @@ class Header extends ImmutablePureComponent {
}
render () {
const { account, intl, domain, identity_proofs } = this.props;
const { account, intl, domain, identity_proofs, hideProfile } = this.props;
if (!account) {
return null;
@ -390,60 +391,62 @@ class Header extends ImmutablePureComponent {
</div>
<div className='account__header__extra'>
<div className='account__header__bio'>
{(fields.size > 0 || identity_proofs.size > 0) && (
<div className='account__header__fields'>
{identity_proofs.map((proof, i) => (
<dl key={i}>
<dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} />
{!hideProfile && (
<div className='account__header__bio'>
{(fields.size > 0 || identity_proofs.size > 0) && (
<div className='account__header__fields'>
{identity_proofs.map((proof, i) => (
<dl key={i}>
<dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} />
<dd className='verified'>
<a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
<Icon id='check' className='verified__mark' />
</span></a>
<a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
</dd>
</dl>
))}
{fields.map((pair, i) => (
<dl key={i}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
<dd className='verified'>
<a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
<Icon id='check' className='verified__mark' />
</span></a>
<a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
</dd>
</dl>
))}
{fields.map((pair, i) => (
<dl key={i}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
<dd className={`${pair.get('verified_at') ? 'verified' : ''} translate`} title={pair.get('value_plain')}>
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
</dd>
</dl>
))}
<dd className={`${pair.get('verified_at') ? 'verified' : ''} translate`} title={pair.get('value_plain')}>
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
</dd>
</dl>
))}
</div>
)}
{account.get('id') !== me && !suspended && <AccountNoteContainer account={account} />}
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
<div className='account__header__personal--wrapper'>
<table className='account__header__personal'>
<tbody>
{location && <tr>
<th><Icon id='map-marker' fixedWidth aria-hidden='true' /> <FormattedMessage id='account.location' defaultMessage='Location' /></th>
<td>{location}</td>
</tr>}
{birthday && <tr>
<th><Icon id='birthday-cake' fixedWidth aria-hidden='true' /> <FormattedMessage id='account.birthday' defaultMessage='Birthday' /></th>
<td><FormattedDate value={birthday} hour12={false} year='numeric' month='short' day='2-digit' />(<FormattedMessage id='account.age' defaultMessage='{age} years old}' values={{age: age(birthday)}} />)</td>
</tr>}
<tr>
<th><Icon id='calendar' fixedWidth aria-hidden='true' /> <FormattedMessage id='account.joined' defaultMessage='Joined' /></th>
<td><FormattedDate value={joined} hour12={false} year='numeric' month='short' day='2-digit' /></td>
</tr>
</tbody>
</table>
</div>
)}
{account.get('id') !== me && !suspended && <AccountNoteContainer account={account} />}
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
<div className='account__header__personal--wrapper'>
<table className='account__header__personal'>
<tbody>
{location && <tr>
<th><Icon id='map-marker' fixedWidth aria-hidden='true' /> <FormattedMessage id='account.location' defaultMessage='Location' /></th>
<td>{location}</td>
</tr>}
{birthday && <tr>
<th><Icon id='birthday-cake' fixedWidth aria-hidden='true' /> <FormattedMessage id='account.birthday' defaultMessage='Birthday' /></th>
<td><FormattedDate value={birthday} hour12={false} year='numeric' month='short' day='2-digit' />(<FormattedMessage id='account.age' defaultMessage='{age} years old}' values={{age: age(birthday)}} />)</td>
</tr>}
<tr>
<th><Icon id='calendar' fixedWidth aria-hidden='true' /> <FormattedMessage id='account.joined' defaultMessage='Joined' /></th>
<td><FormattedDate value={joined} hour12={false} year='numeric' month='short' day='2-digit' /></td>
</tr>
</tbody>
</table>
</div>
</div>
)}
{!suspended && (
{!hideProfile && !suspended && (
<div className='account__header__extra__links'>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={hide_statuses_count ? intl.formatMessage(messages.secret) : intl.formatNumber(account.get('statuses_count'))}>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}/posts`} title={hide_statuses_count ? intl.formatMessage(messages.secret) : intl.formatNumber(account.get('statuses_count'))}>
<ShortNumber
hide={hide_statuses_count}
value={account.get('statuses_count')}

View file

@ -0,0 +1,366 @@
import React, { Fragment } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Button from 'mastodon/components/button';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif, me, isStaff, show_followed_by, follow_button_to_list_adder } from 'mastodon/initial_state';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import IconButton from 'mastodon/components/icon_button';
import Avatar from 'mastodon/components/avatar';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe' },
subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe' },
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', defaultMessage: 'Unblock @{name}' },
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
conversations: { id: 'account.conversations', defaultMessage: 'Show conversations with @{name}' },
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
report: { id: 'account.report', defaultMessage: 'Report @{name}' },
share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
media: { id: 'account.media', defaultMessage: 'Media' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' },
disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
circles: { id: 'navigation_bar.circles', defaultMessage: 'Circles' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
add_or_remove_from_circle: { id: 'account.add_or_remove_from_circle', defaultMessage: 'Add or Remove from circles' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
});
export default @injectIntl
class HeaderCommon extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
onFollow: PropTypes.func.isRequired,
onSubscribe: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onConversations: PropTypes.func.isRequired,
onReblogToggle: PropTypes.func.isRequired,
onNotifyToggle: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired,
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
onEditAccountNote: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
domain: PropTypes.string.isRequired,
};
openEditProfile = () => {
window.open('/settings/profile', '_blank');
}
isStatusesPageActive = (match, location) => {
if (!match) {
return false;
}
return !location.pathname.match(/\/(followers|following)\/?$/);
}
handleMouseEnter = ({ currentTarget }) => {
if (autoPlayGif) {
return;
}
const emojis = currentTarget.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-original');
}
}
handleMouseLeave = ({ currentTarget }) => {
if (autoPlayGif) {
return;
}
const emojis = currentTarget.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-static');
}
}
handleFollow = (e) => {
if ((e && e.shiftKey) ^ !follow_button_to_list_adder) {
this.props.onFollow(this.props.account);
} else {
this.props.onAddToList(this.props.account);
}
}
handleSubscribe = (e) => {
if ((e && e.shiftKey) ^ !follow_button_to_list_adder) {
this.props.onSubscribe(this.props.account);
} else {
this.props.onAddToList(this.props.account);
}
}
setRef = (c) => {
this.node = c;
}
render () {
const { account, intl, domain } = this.props;
if (!account) {
return null;
}
const suspended = account.get('suspended');
let info = [];
let actionBtn = '';
let bellBtn = '';
let lockedIcon = '';
let menu = [];
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
info.push(<span key='followed_by' className='relationship-tag'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>);
} else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) {
info.push(<span key='blocked' className='relationship-tag'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>);
}
if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) {
info.push(<span key='muted' className='relationship-tag'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>);
} else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) {
info.push(<span key='domain_blocked' className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain blocked' /></span>);
}
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
}
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', { 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
} else if (!account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
}
} else {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
}
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
actionBtn = '';
}
if (account.get('locked')) {
lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />;
}
if (account.get('id') !== me) {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
menu.push(null);
}
menu.push({ text: intl.formatMessage(messages.conversations, { name: account.get('username') }), action: this.props.onConversations });
menu.push(null);
if ('share' in navigator) {
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
menu.push(null);
}
if (account.get('id') === me) {
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
menu.push({ text: intl.formatMessage(messages.circles), to: '/circles' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
} else {
if (account.getIn(['relationship', 'following'])) {
if (!account.getIn(['relationship', 'muting'])) {
if (account.getIn(['relationship', 'showing_reblogs'])) {
menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
} else {
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
}
}
menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
menu.push(null);
}
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
menu.push(null);
if (account.getIn(['relationship', 'followed_by'])) {
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_circle), action: this.props.onAddToCircle });
menu.push(null);
}
if (account.getIn(['relationship', 'muting'])) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
} else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute });
}
if (account.getIn(['relationship', 'blocking'])) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
} else {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock });
}
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport });
}
if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];
menu.push(null);
if (account.getIn(['relationship', 'domain_blocking'])) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.props.onUnblockDomain });
} else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.props.onBlockDomain });
}
}
if (account.get('id') !== me && isStaff) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` });
}
const displayNameHtml = { __html: account.get('display_name_html') };
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
let badge;
if (account.get('bot')) {
badge = (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>);
} else if (account.get('group')) {
badge = (<div className='account-role group'><FormattedMessage id='account.badges.group' defaultMessage='Group' /></div>);
} else {
badge = null;
}
const following = account.getIn(['relationship', 'following']);
const delivery = account.getIn(['relationship', 'delivery_following']);
const followed_by = account.getIn(['relationship', 'followed_by']) && show_followed_by;
const subscribing = account.getIn(['relationship', 'subscribing'], new Map).size > 0;
const subscribing_home = account.getIn(['relationship', 'subscribing', '-1'], new Map).size > 0;
const blockd_by = account.getIn(['relationship', 'blocked_by']);
let buttons;
if(me !== account.get('id') && !blockd_by) {
let following_buttons, subscribing_buttons;
if(!account.get('moved') || subscribing) {
subscribing_buttons = (
<IconButton
icon='rss-square'
title={intl.formatMessage(
subscribing ? messages.unsubscribe : messages.subscribe
)}
onClick={this.handleSubscribe}
active={subscribing}
no_delivery={subscribing && !subscribing_home}
/>
);
}
if(!account.get('moved') || following) {
following_buttons = (
<IconButton
icon={following ? 'user-times' : 'user-plus'}
title={intl.formatMessage(
following ? messages.unfollow : messages.follow
)}
onClick={this.handleFollow}
active={following}
passive={followed_by}
no_delivery={following && !delivery}
/>
);
}
buttons = <Fragment>{subscribing_buttons}{following_buttons}</Fragment>;
}
return (
<div className={classNames('account__header', 'advanced', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className='account__header__image'>
<div className='account__header__info'>
{!suspended && info}
</div>
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
</div>
<div className='account__header__bar'>
<div className='account__header__tabs'>
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
<Avatar account={account} size={90} />
</a>
<div className='spacer' />
{!suspended && (
<div className='account__header__tabs__buttons'>
{actionBtn}
{bellBtn}
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
</div>
)}
</div>
<div className='account__header__tabs__name'>
<h1>
<span dangerouslySetInnerHTML={displayNameHtml} /> {badge}
<small>@{acct} {lockedIcon}</small>
</h1>
<div className='account__header__tabs__name__relationship account__relationship'>
{buttons}
</div>
</div>
</div>
</div>
);
}
}

View file

@ -0,0 +1,116 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from 'mastodon/initial_state';
import Icon from 'mastodon/components/icon';
import AccountNoteContainer from '../containers/account_note_container';
import age from 's-age';
import classNames from 'classnames';
const messages = defineMessages({
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
});
const dateFormatOptions = {
month: 'short',
day: 'numeric',
year: 'numeric',
hour12: false,
hour: '2-digit',
minute: '2-digit',
};
export default @injectIntl
class HeaderExtra extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
identity_proofs: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
};
isStatusesPageActive = (match, location) => {
if (!match) {
return false;
}
return !location.pathname.match(/\/(followers|following)\/?$/);
}
render () {
const { account, intl, identity_proofs } = this.props;
if (!account) {
return null;
}
const suspended = account.get('suspended');
const content = { __html: account.get('note_emojified') };
const fields = account.get('fields');
const location = account.getIn(['other_settings', 'location']);
const birthday = account.getIn(['other_settings', 'birthday']);
const joined = account.get('created_at');
return (
<div className={classNames('account__header', 'advanced', { inactive: !!account.get('moved') })}>
<div className='account__header__extra'>
<div className='account__header__bio'>
{(fields.size > 0 || identity_proofs.size > 0) && (
<div className='account__header__fields'>
{identity_proofs.map((proof, i) => (
<dl key={i}>
<dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} />
<dd className='verified'>
<a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
<Icon id='check' className='verified__mark' />
</span></a>
<a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
</dd>
</dl>
))}
{fields.map((pair, i) => (
<dl key={i}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
<dd className={`${pair.get('verified_at') ? 'verified' : ''} translate`} title={pair.get('value_plain')}>
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
</dd>
</dl>
))}
</div>
)}
{account.get('id') !== me && !suspended && <AccountNoteContainer account={account} />}
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}
<div className='account__header__personal--wrapper'>
<table className='account__header__personal'>
<tbody>
{location && <tr>
<th><Icon id='map-marker' fixedWidth aria-hidden='true' /> <FormattedMessage id='account.location' defaultMessage='Location' /></th>
<td>{location}</td>
</tr>}
{birthday && <tr>
<th><Icon id='birthday-cake' fixedWidth aria-hidden='true' /> <FormattedMessage id='account.birthday' defaultMessage='Birthday' /></th>
<td><FormattedDate value={birthday} hour12={false} year='numeric' month='short' day='2-digit' />(<FormattedMessage id='account.age' defaultMessage='{age} years old}' values={{ age: age(birthday) }} />)</td>
</tr>}
<tr>
<th><Icon id='calendar' fixedWidth aria-hidden='true' /> <FormattedMessage id='account.joined' defaultMessage='Joined' /></th>
<td><FormattedDate value={joined} hour12={false} year='numeric' month='short' day='2-digit' /></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
);
}
}

View file

@ -0,0 +1,89 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from 'mastodon/initial_state';
import { counterRenderer } from 'mastodon/components/common_counter';
import ShortNumber from 'mastodon/components/short_number';
import { NavLink } from 'react-router-dom';
import classNames from 'classnames';
const messages = defineMessages({
secret: { id: 'account.secret', defaultMessage: 'Secret' },
});
export default @injectIntl
class HeaderExtraLinks extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
intl: PropTypes.object.isRequired,
};
isStatusesPageActive = (match, location) => {
if (!match) {
return false;
}
return !location.pathname.match(/\/(followers|following|subscribing)\/?$/);
}
render () {
const { account, intl } = this.props;
if (!account) {
return null;
}
const suspended = account.get('suspended');
const hide_statuses_count = account.getIn(['other_settings', 'hide_statuses_count'], false);
const hide_following_count = account.getIn(['other_settings', 'hide_following_count'], false);
const hide_followers_count = account.getIn(['other_settings', 'hide_followers_count'], false);
return (
<div className={classNames('account__header', 'advanced', { inactive: !!account.get('moved') })}>
<div className='account__header__extra'>
{!suspended && (
<div className='account__header__extra__links'>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}/posts`} title={hide_statuses_count ? intl.formatMessage(messages.secret) : intl.formatNumber(account.get('statuses_count'))}>
<ShortNumber
hide={hide_statuses_count}
value={account.get('statuses_count')}
renderer={counterRenderer('statuses')}
/>
</NavLink>
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={hide_following_count ? intl.formatMessage(messages.secret) : intl.formatNumber(account.get('following_count'))}>
<ShortNumber
hide={hide_following_count}
value={account.get('following_count')}
renderer={counterRenderer('following')}
/>
</NavLink>
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={hide_followers_count ? intl.formatMessage(messages.secret) : intl.formatNumber(account.get('followers_count'))}>
<ShortNumber
hide={hide_followers_count}
value={account.get('followers_count')}
renderer={counterRenderer('followers')}
/>
</NavLink>
{ (me === account.get('id')) && (
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/subscribing`} title={intl.formatNumber(account.get('subscribing_count'))}>
<ShortNumber
value={account.get('subscribing_count')}
renderer={counterRenderer('subscribers')}
/>
</NavLink>
)}
</div>
)}
</div>
</div>
);
}
}

View file

@ -6,30 +6,37 @@ import { fetchAccount } from '../../actions/accounts';
import { expandAccountCoversations } from '../../actions/timelines';
import StatusList from '../../components/status_list';
import LoadingIndicator from '../../components/loading_indicator';
import Column from '../ui/components/column';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import ColumnSettingsContainer from '../account_timeline/containers/column_settings_container';
import HeaderContainer from '../account_timeline/containers/header_container';
import ColumnBackButton from '../../components/column_back_button';
import { List as ImmutableList } from 'immutable';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
import MissingIndicator from 'mastodon/components/missing_indicator';
import TimelineHint from 'mastodon/components/timeline_hint';
import { new_features_policy } from 'mastodon/initial_state';
const messages = defineMessages({
title: { id: 'column.account', defaultMessage: 'Account' },
});
const emptyList = ImmutableList();
const mapStateToProps = (state, { params: { accountId } }) => {
return {
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
remoteUrl: state.getIn(['accounts', accountId, 'url']),
isAccount: !!state.getIn(['accounts', accountId]),
statusIds: state.getIn(['timelines', `account:${accountId}:conversations`, 'items'], emptyList),
isLoading: state.getIn(['timelines', `account:${accountId}:conversations`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${accountId}:conversations`, 'hasMore']),
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
};
};
const mapStateToProps = (state, { params: { accountId } }) => ({
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
remoteUrl: state.getIn(['accounts', accountId, 'url']),
isAccount: !!state.getIn(['accounts', accountId]),
statusIds: state.getIn(['timelines', `account:${accountId}:conversations`, 'items'], emptyList),
isLoading: state.getIn(['timelines', `account:${accountId}:conversations`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${accountId}:conversations`, 'hasMore']),
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
advancedMode: state.getIn(['settings', 'account', 'other', 'advancedMode'], new_features_policy === 'conservative' ? false : true),
hideRelation: state.getIn(['settings', 'account', 'other', 'hideRelation'], false),
});
const RemoteHint = ({ url }) => (
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.statuses' defaultMessage='Older toots' />} />
@ -40,6 +47,7 @@ RemoteHint.propTypes = {
};
export default @connect(mapStateToProps)
@injectIntl
class AccountConversations extends ImmutablePureComponent {
static propTypes = {
@ -49,11 +57,11 @@ class AccountConversations extends ImmutablePureComponent {
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
withReplies: PropTypes.bool,
advancedMode: PropTypes.bool,
hideRelation: PropTypes.bool,
blockedBy: PropTypes.bool,
isAccount: PropTypes.bool,
suspended: PropTypes.bool,
remote: PropTypes.bool,
remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool,
};
@ -81,8 +89,16 @@ class AccountConversations extends ImmutablePureComponent {
this.props.dispatch(expandAccountCoversations(this.props.params.accountId, { maxId }));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
render () {
const { statusIds, isLoading, hasMore, blockedBy, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props;
const { statusIds, isLoading, hasMore, blockedBy, suspended, isAccount, multiColumn, hideRelation, intl } = this.props;
if (!isAccount) {
return (
@ -107,22 +123,26 @@ class AccountConversations extends ImmutablePureComponent {
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
} else if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (remote && statusIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />;
} else {
emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />;
emptyMessage = <FormattedMessage id='empty_column.conversation_unavailable' defaultMessage='No conversation with this user yet' />;
}
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
return (
<Column>
<ColumnBackButton multiColumn={multiColumn} />
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='user'
active={false}
title={intl.formatMessage(messages.title)}
onClick={this.handleHeaderClick}
pinned={false}
multiColumn={multiColumn}
>
<ColumnSettingsContainer />
</ColumnHeader>
<StatusList
prepend={<HeaderContainer accountId={this.props.params.accountId} />}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideProfile hideRelation={hideRelation} hideFeaturedTags />}
alwaysPrepend
append={remoteMessage}
scrollKey='account_conversations'
statusIds={(suspended || blockedBy) ? emptyList : statusIds}
isLoading={isLoading}

View file

@ -5,8 +5,9 @@ import PropTypes from 'prop-types';
import { fetchAccount } from 'mastodon/actions/accounts';
import { expandAccountMediaTimeline } from '../../actions/timelines';
import LoadingIndicator from 'mastodon/components/loading_indicator';
import Column from '../ui/components/column';
import ColumnBackButton from 'mastodon/components/column_back_button';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import ColumnSettingsContainer from '../account_timeline/containers/column_settings_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { getAccountGallery } from 'mastodon/selectors';
import MediaItem from './components/media_item';
@ -15,7 +16,12 @@ import ScrollContainer from 'mastodon/containers/scroll_container';
import LoadMore from 'mastodon/components/load_more';
import MissingIndicator from 'mastodon/components/missing_indicator';
import { openModal } from 'mastodon/actions/modal';
import { FormattedMessage } from 'react-intl';
import { new_features_policy } from 'mastodon/initial_state';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
const messages = defineMessages({
title: { id: 'column.account', defaultMessage: 'Account' },
});
const mapStateToProps = (state, props) => ({
isAccount: !!state.getIn(['accounts', props.params.accountId]),
@ -24,6 +30,8 @@ const mapStateToProps = (state, props) => ({
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
suspended: state.getIn(['accounts', props.params.accountId, 'suspended'], false),
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
advancedMode: state.getIn(['settings', 'account', 'other', 'advancedMode'], new_features_policy === 'conservative' ? false : true),
hideRelation: state.getIn(['settings', 'account', 'other', 'hideRelation'], false),
});
class LoadMoreMedia extends ImmutablePureComponent {
@ -49,6 +57,7 @@ class LoadMoreMedia extends ImmutablePureComponent {
}
export default @connect(mapStateToProps)
@injectIntl
class AccountGallery extends ImmutablePureComponent {
static propTypes = {
@ -58,6 +67,8 @@ class AccountGallery extends ImmutablePureComponent {
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
isAccount: PropTypes.bool,
advancedMode: PropTypes.bool,
hideRelation: PropTypes.bool,
blockedBy: PropTypes.bool,
suspended: PropTypes.bool,
multiColumn: PropTypes.bool,
@ -125,8 +136,16 @@ class AccountGallery extends ImmutablePureComponent {
}
}
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
render () {
const { attachments, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;
const { attachments, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended, hideRelation, intl } = this.props;
const { width } = this.state;
if (!isAccount) {
@ -160,12 +179,21 @@ class AccountGallery extends ImmutablePureComponent {
}
return (
<Column>
<ColumnBackButton multiColumn={multiColumn} />
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='user'
active={false}
title={intl.formatMessage(messages.title)}
onClick={this.handleHeaderClick}
pinned={false}
multiColumn={multiColumn}
>
<ColumnSettingsContainer />
</ColumnHeader>
<ScrollContainer scrollKey='account_gallery'>
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
<HeaderContainer accountId={this.props.params.accountId} />
<HeaderContainer accountId={this.props.params.accountId} hideProfile hideRelation={hideRelation} hideFeaturedTags />
{(suspended || blockedBy) ? (
<div className='empty-column-indicator'>

View file

@ -0,0 +1,40 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, FormattedMessage } from 'react-intl';
import SettingToggle from '../../notifications/components/setting_toggle';
export default @injectIntl
class ColumnSettings extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
advancedMode: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
render () {
const { settings, advancedMode, onChange } = this.props;
return (
<div>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['other', 'advancedMode']} onChange={onChange} label={<FormattedMessage id='account.column_settings.advanced_mode' defaultMessage='Advanced mode' />} />
{advancedMode && (
<Fragment>
<span className='column-settings__section'><FormattedMessage id='account.column_settings.advanced_settings' defaultMessage='Advanced settings' /></span>
<SettingToggle settings={settings} settingPath={['other', 'openPostsFirst']} onChange={onChange} label={<FormattedMessage id='account.column_settings.open_posts_first' defaultMessage='Open posts first' />} />
<SettingToggle settings={settings} settingPath={['other', 'withoutReblogs']} onChange={onChange} label={<FormattedMessage id='account.column_settings.without_reblogs' defaultMessage='Without boosts' />} />
<SettingToggle settings={settings} settingPath={['other', 'showPostsInAbout']} onChange={onChange} label={<FormattedMessage id='account.column_settings.show_posts_in_about' defaultMessage='Show posts in about' />} />
<SettingToggle settings={settings} settingPath={['other', 'hideFeaturedTags']} onChange={onChange} label={<FormattedMessage id='account.column_settings.hide_featured_tags' defaultMessage='Hide featuread tags selection' />} />
<SettingToggle settings={settings} settingPath={['other', 'hideRelation']} onChange={onChange} label={<FormattedMessage id='account.column_settings.hide_relation' defaultMessage='Hide post and follow counters (except about)' />} />
</Fragment>
)}
</div>
</div>
);
}
}

View file

@ -1,12 +1,17 @@
import React from 'react';
import React, { Fragment } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import InnerHeader from '../../account/components/header';
import InnerHeaderCommon from '../../account/components/header_common';
import InnerHeaderExtra from '../../account/components/header_extra';
import InnerHeaderExtraLinks from '../../account/components/header_extra_links';
import FeaturedTags from '../../account/components/featured_tags';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { follow_button_to_list_adder } from 'mastodon/initial_state';
import MovedNote from './moved_note';
import { FormattedMessage } from 'react-intl';
import { NavLink } from 'react-router-dom';
import Icon from 'mastodon/components/icon';
export default class Header extends ImmutablePureComponent {
@ -28,10 +33,18 @@ export default class Header extends ImmutablePureComponent {
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
onAddToCircle: PropTypes.func.isRequired,
hideTabs: PropTypes.bool,
hideRelation: PropTypes.bool,
hideProfile: PropTypes.bool,
advancedMode: PropTypes.bool,
tagged: PropTypes.string,
hideFeaturedTags: PropTypes.bool,
domain: PropTypes.string.isRequired,
};
static defaultProps = {
hideProfile: false,
};
static contextTypes = {
router: PropTypes.object,
};
@ -117,7 +130,7 @@ export default class Header extends ImmutablePureComponent {
}
render () {
const { account, hideTabs, identity_proofs } = this.props;
const { account, hideRelation, identity_proofs, hideProfile, advancedMode, tagged, hideFeaturedTags } = this.props;
if (account === null) {
return null;
@ -127,34 +140,72 @@ export default class Header extends ImmutablePureComponent {
<div className='account-timeline__header'>
{account.get('moved') && <MovedNote from={account} to={account.get('moved')} />}
<InnerHeader
account={account}
identity_proofs={identity_proofs}
onFollow={this.handleFollow}
onSubscribe={this.handleSubscribe}
onBlock={this.handleBlock}
onMention={this.handleMention}
onDirect={this.handleDirect}
onConversations={this.handleConversations}
onReblogToggle={this.handleReblogToggle}
onNotifyToggle={this.handleNotifyToggle}
onReport={this.handleReport}
onMute={this.handleMute}
onBlockDomain={this.handleBlockDomain}
onUnblockDomain={this.handleUnblockDomain}
onEndorseToggle={this.handleEndorseToggle}
onAddToList={this.handleAddToList}
onAddToCircle={this.handleAddToCircle}
onEditAccountNote={this.handleEditAccountNote}
domain={this.props.domain}
/>
{advancedMode ? (
<Fragment>
<InnerHeaderCommon
account={account}
onFollow={this.handleFollow}
onSubscribe={this.handleSubscribe}
onBlock={this.handleBlock}
onMention={this.handleMention}
onDirect={this.handleDirect}
onConversations={this.handleConversations}
onReblogToggle={this.handleReblogToggle}
onNotifyToggle={this.handleNotifyToggle}
onReport={this.handleReport}
onMute={this.handleMute}
onBlockDomain={this.handleBlockDomain}
onUnblockDomain={this.handleUnblockDomain}
onEndorseToggle={this.handleEndorseToggle}
onAddToList={this.handleAddToList}
onAddToCircle={this.handleAddToCircle}
onEditAccountNote={this.handleEditAccountNote}
domain={this.props.domain}
/>
{!hideTabs && (
<div className='account__section-headline'>
<NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots and replies' /></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
</div>
<div className='account__section-headline with-short-label'>
<NavLink exact to={`/accounts/${account.get('id')}/posts`}><Icon id='home' fixedWidth /><span className='account__section-headline__short-label'><FormattedMessage id='account.short.posts' defaultMessage='Posts' children={msg=> <>{msg}</>} /></span></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><Icon id='reply-all' fixedWidth /><span className='account__section-headline__short-label'><FormattedMessage id='account.short.with_replies' defaultMessage='Posts & Replies' children={msg=> <>{msg}</>} /></span></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/media`}><Icon id='picture-o' fixedWidth /><span className='account__section-headline__short-label'><FormattedMessage id='account.short.media' defaultMessage='Media' children={msg=> <>{msg}</>} /></span></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/conversations`}><Icon id='at' fixedWidth /><span className='account__section-headline__short-label'><FormattedMessage id='account.short.conversations' defaultMessage='Conversations' children={msg=> <>{msg}</>} /></span></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/about`}><Icon id='address-card-o' fixedWidth /><span className='account__section-headline__short-label'><FormattedMessage id='account.short.about' defaultMessage='About' children={msg=> <>{msg}</>} /></span></NavLink>
</div>
{hideProfile && !hideFeaturedTags && <FeaturedTags account={account} tagged={tagged} />}
{!hideRelation && <InnerHeaderExtraLinks account={account} />}
{!hideProfile && <InnerHeaderExtra account={account} identity_proofs={identity_proofs} />}
</Fragment>
) : (
<Fragment>
<InnerHeader
account={account}
identity_proofs={identity_proofs}
onFollow={this.handleFollow}
onSubscribe={this.handleSubscribe}
onBlock={this.handleBlock}
onMention={this.handleMention}
onDirect={this.handleDirect}
onConversations={this.handleConversations}
onReblogToggle={this.handleReblogToggle}
onNotifyToggle={this.handleNotifyToggle}
onReport={this.handleReport}
onMute={this.handleMute}
onBlockDomain={this.handleBlockDomain}
onUnblockDomain={this.handleUnblockDomain}
onEndorseToggle={this.handleEndorseToggle}
onAddToList={this.handleAddToList}
onAddToCircle={this.handleAddToCircle}
onEditAccountNote={this.handleEditAccountNote}
domain={this.props.domain}
hideProfile={hideProfile}
/>
<div className='account__section-headline'>
<NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots and replies' /></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
</div>
</Fragment>
)}
</div>
);

View file

@ -0,0 +1,18 @@
import { connect } from 'react-redux';
import ColumnSettings from '../components/column_settings';
import { changeSetting } from '../../../actions/settings';
const mapStateToProps = (state) => ({
settings: state.getIn(['settings', 'account']),
advancedMode: state.getIn(['settings', 'account', 'other', 'advancedMode'], false),
});
const mapDispatchToProps = (dispatch) => {
return {
onChange (key, checked) {
dispatch(changeSetting(['account', ...key], checked));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);

View file

@ -34,10 +34,12 @@ const messages = defineMessages({
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, { accountId }) => ({
const mapStateToProps = (state, { accountId, hideTabs, hideProfile }) => ({
account: getAccount(state, accountId),
domain: state.getIn(['meta', 'domain']),
identity_proofs: state.getIn(['identity_proofs', accountId], ImmutableList()),
hideProfile: hideTabs || hideProfile,
advancedMode: state.getIn(['settings', 'account', 'other', 'advancedMode'], false),
});
return mapStateToProps;

View file

@ -6,33 +6,54 @@ import { fetchAccount } from '../../actions/accounts';
import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
import StatusList from '../../components/status_list';
import LoadingIndicator from '../../components/loading_indicator';
import Column from '../ui/components/column';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import ColumnSettingsContainer from './containers/column_settings_container';
import HeaderContainer from './containers/header_container';
import ColumnBackButton from '../../components/column_back_button';
import { List as ImmutableList } from 'immutable';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
import MissingIndicator from 'mastodon/components/missing_indicator';
import TimelineHint from 'mastodon/components/timeline_hint';
import { me } from 'mastodon/initial_state';
import { me, new_features_policy } from 'mastodon/initial_state';
import { connectTimeline, disconnectTimeline } from 'mastodon/actions/timelines';
import { fetchFeaturedTags } from '../../actions/featured_tags';
const emptyList = ImmutableList();
const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => {
const path = withReplies ? `${accountId}:with_replies` : accountId;
const messages = defineMessages({
title: { id: 'column.account', defaultMessage: 'Account' },
});
const mapStateToProps = (state, { params: { accountId, tagged }, about, withReplies, posts }) => {
posts = tagged ? false : posts;
withReplies = tagged ? true : withReplies;
const advancedMode = state.getIn(['settings', 'account', 'other', 'advancedMode'], new_features_policy === 'conservative' ? false : true);
const hideFeaturedTags = state.getIn(['settings', 'account', 'other', 'hideFeaturedTags'], false);
const withoutReblogs = advancedMode && state.getIn(['settings', 'account', 'other', 'withoutReblogs'], false);
const showPostsInAbout = state.getIn(['settings', 'account', 'other', 'showPostsInAbout'], true);
const hideRelation = state.getIn(['settings', 'account', 'other', 'hideRelation'], false);
const path = `${accountId}${withReplies ? ':with_replies' : ''}${withoutReblogs ? ':without_reblogs' : ''}${tagged ? `:${tagged}` : ''}`;
return {
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
remoteUrl: state.getIn(['accounts', accountId, 'url']),
isAccount: !!state.getIn(['accounts', accountId]),
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], emptyList),
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList),
statusIds: advancedMode && about && !showPostsInAbout ? emptyList : state.getIn(['timelines', `account:${path}`, 'items'], emptyList),
featuredStatusIds: (withReplies || posts) ? emptyList : state.getIn(['timelines', `account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, 'items'], emptyList),
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
advancedMode,
hideFeaturedTags,
posts,
withReplies,
withoutReblogs,
showPostsInAbout,
hideRelation,
};
};
@ -45,16 +66,24 @@ RemoteHint.propTypes = {
};
export default @connect(mapStateToProps)
@injectIntl
class AccountTimeline extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list,
featuredStatusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
about: PropTypes.bool,
withReplies: PropTypes.bool,
withoutReblogs: PropTypes.bool,
posts: PropTypes.bool,
advancedMode: PropTypes.bool,
hideFeaturedTags: PropTypes.bool,
hideRelation: PropTypes.bool,
blockedBy: PropTypes.bool,
isAccount: PropTypes.bool,
suspended: PropTypes.bool,
@ -63,17 +92,29 @@ class AccountTimeline extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
static defaultProps = {
about: false,
withReplies: false,
posts: false,
};
componentWillMount () {
const { params: { accountId }, withReplies, dispatch } = this.props;
const { params: { accountId, tagged }, about, withReplies, posts, advancedMode, hideFeaturedTags, withoutReblogs, showPostsInAbout, dispatch } = this.props;
dispatch(fetchAccount(accountId));
dispatch(fetchAccountIdentityProofs(accountId));
if (!withReplies) {
dispatch(expandAccountFeaturedTimeline(accountId));
if (!withReplies && !posts) {
dispatch(expandAccountFeaturedTimeline(accountId, { tagged }));
}
dispatch(expandAccountTimeline(accountId, { withReplies }));
if (!about || !advancedMode || showPostsInAbout) {
dispatch(expandAccountTimeline(accountId, { withReplies, tagged, withoutReblogs }));
}
if (tagged || !hideFeaturedTags) {
dispatch(fetchFeaturedTags(accountId));
}
if (accountId === me) {
dispatch(connectTimeline(`account:${me}`));
@ -83,18 +124,29 @@ class AccountTimeline extends ImmutablePureComponent {
componentWillReceiveProps (nextProps) {
const { dispatch } = this.props;
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId)
|| (nextProps.params.tagged !== this.props.params.tagged)
|| nextProps.withReplies !== this.props.withReplies
|| nextProps.withoutReblogs !== this.props.withoutReblogs
|| nextProps.showPostsInAbout !== this.props.showPostsInAbout
) {
dispatch(fetchAccount(nextProps.params.accountId));
dispatch(fetchAccountIdentityProofs(nextProps.params.accountId));
if (!nextProps.withReplies) {
dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
if (!nextProps.withReplies && !nextProps.posts) {
dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId, { tagged: nextProps.params.tagged }));
}
dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies }));
if (!nextProps.about || nextProps.showPostsInAbout) {
dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.withReplies, tagged: nextProps.params.tagged, withoutReblogs: nextProps.withoutReblogs }));
}
if (nextProps.params.tagged || !nextProps.hideFeaturedTags) {
dispatch(fetchFeaturedTags(nextProps.params.accountId));
}
}
if (nextProps.params.accountId === me && this.props.params.accountId !== me) {
if ((!nextProps.about || !this.props.advancedMode) && nextProps.params.accountId === me && this.props.params.accountId !== me) {
dispatch(connectTimeline(`account:${me}`));
} else if (this.props.params.accountId === me && nextProps.params.accountId !== me) {
dispatch(disconnectTimeline(`account:${me}`));
@ -110,11 +162,19 @@ class AccountTimeline extends ImmutablePureComponent {
}
handleLoadMore = maxId => {
this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies }));
this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies, tagged: this.props.params.tagged, withoutReblogs: this.props.withoutReblogs }));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
render () {
const { statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props;
const { intl, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, multiColumn, remote, remoteUrl, about, withReplies, posts, advancedMode, hideFeaturedTags, showPostsInAbout, hideRelation } = this.props;
if (!isAccount) {
return (
@ -139,28 +199,39 @@ class AccountTimeline extends ImmutablePureComponent {
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
} else if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (about && advancedMode && featuredStatusIds.isEmpty()) {
emptyMessage = <FormattedMessage id='empty_column.pinned_unavailable' defaultMessage='Pinned posts unavailable' />;
} else if (remote && statusIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />;
} else {
emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />;
}
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
const remoteMessage = (!about && remote) ? <RemoteHint url={remoteUrl} /> : null;
return (
<Column>
<ColumnBackButton multiColumn={multiColumn} />
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='user'
active={false}
title={intl.formatMessage(messages.title)}
onClick={this.handleHeaderClick}
pinned={false}
multiColumn={multiColumn}
>
<ColumnSettingsContainer />
</ColumnHeader>
<StatusList
prepend={<HeaderContainer accountId={this.props.params.accountId} />}
prepend={<HeaderContainer accountId={this.props.params.accountId} tagged={this.props.params.tagged} hideProfile={withReplies || posts || !!this.props.params.tagged} hideRelation={!about && hideRelation} hideFeaturedTags={hideFeaturedTags} />}
alwaysPrepend
append={remoteMessage}
scrollKey='account_timeline'
statusIds={(suspended || blockedBy) ? emptyList : statusIds}
featuredStatusIds={featuredStatusIds}
isLoading={isLoading}
hasMore={hasMore}
onLoadMore={this.handleLoadMore}
hasMore={about && advancedMode && !showPostsInAbout ? null : hasMore}
onLoadMore={about && advancedMode && !showPostsInAbout ? null : this.handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
timelineId='account'

View file

@ -10,14 +10,20 @@ import {
fetchFollowers,
expandFollowers,
} from '../../actions/accounts';
import { FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import ColumnSettingsContainer from '../account_timeline/containers/column_settings_container';
import HeaderContainer from '../account_timeline/containers/header_container';
import ColumnBackButton from '../../components/column_back_button';
import ScrollableList from '../../components/scrollable_list';
import MissingIndicator from 'mastodon/components/missing_indicator';
import TimelineHint from 'mastodon/components/timeline_hint';
import { new_features_policy } from 'mastodon/initial_state';
const messages = defineMessages({
title: { id: 'column.account', defaultMessage: 'Account' },
});
const mapStateToProps = (state, props) => ({
remote: !!(state.getIn(['accounts', props.params.accountId, 'acct']) !== state.getIn(['accounts', props.params.accountId, 'username'])),
@ -27,6 +33,7 @@ const mapStateToProps = (state, props) => ({
hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']),
isLoading: state.getIn(['user_lists', 'followers', props.params.accountId, 'isLoading'], true),
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
advancedMode: state.getIn(['settings', 'account', 'other', 'advancedMode'], new_features_policy === 'conservative' ? false : true),
});
const RemoteHint = ({ url }) => (
@ -38,12 +45,14 @@ RemoteHint.propTypes = {
};
export default @connect(mapStateToProps)
@injectIntl
class Followers extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
advancedMode: PropTypes.bool,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
blockedBy: PropTypes.bool,
@ -71,8 +80,16 @@ class Followers extends ImmutablePureComponent {
this.props.dispatch(expandFollowers(this.props.params.accountId));
}, 300, { leading: true });
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
render () {
const { accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
const { accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl, intl } = this.props;
if (!isAccount) {
return (
@ -103,15 +120,24 @@ class Followers extends ImmutablePureComponent {
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
return (
<Column>
<ColumnBackButton multiColumn={multiColumn} />
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='user'
active={false}
title={intl.formatMessage(messages.title)}
onClick={this.handleHeaderClick}
pinned={false}
multiColumn={multiColumn}
>
<ColumnSettingsContainer />
</ColumnHeader>
<ScrollableList
scrollKey='followers'
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideProfile hideFeaturedTags />}
alwaysPrepend
append={remoteMessage}
emptyMessage={emptyMessage}

View file

@ -10,14 +10,20 @@ import {
fetchFollowing,
expandFollowing,
} from '../../actions/accounts';
import { FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import ColumnSettingsContainer from '../account_timeline/containers/column_settings_container';
import HeaderContainer from '../account_timeline/containers/header_container';
import ColumnBackButton from '../../components/column_back_button';
import ScrollableList from '../../components/scrollable_list';
import MissingIndicator from 'mastodon/components/missing_indicator';
import TimelineHint from 'mastodon/components/timeline_hint';
import { new_features_policy } from 'mastodon/initial_state';
const messages = defineMessages({
title: { id: 'column.account', defaultMessage: 'Account' },
});
const mapStateToProps = (state, props) => ({
remote: !!(state.getIn(['accounts', props.params.accountId, 'acct']) !== state.getIn(['accounts', props.params.accountId, 'username'])),
@ -27,6 +33,7 @@ const mapStateToProps = (state, props) => ({
hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']),
isLoading: state.getIn(['user_lists', 'following', props.params.accountId, 'isLoading'], true),
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
advancedMode: state.getIn(['settings', 'account', 'other', 'advancedMode'], new_features_policy === 'conservative' ? false : true),
});
const RemoteHint = ({ url }) => (
@ -38,12 +45,14 @@ RemoteHint.propTypes = {
};
export default @connect(mapStateToProps)
@injectIntl
class Following extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
advancedMode: PropTypes.bool,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
blockedBy: PropTypes.bool,
@ -71,8 +80,16 @@ class Following extends ImmutablePureComponent {
this.props.dispatch(expandFollowing(this.props.params.accountId));
}, 300, { leading: true });
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
render () {
const { accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
const { accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl, intl } = this.props;
if (!isAccount) {
return (
@ -103,15 +120,24 @@ class Following extends ImmutablePureComponent {
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
return (
<Column>
<ColumnBackButton multiColumn={multiColumn} />
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='user'
active={false}
title={intl.formatMessage(messages.title)}
onClick={this.handleHeaderClick}
pinned={false}
multiColumn={multiColumn}
>
<ColumnSettingsContainer />
</ColumnHeader>
<ScrollableList
scrollKey='following'
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideProfile hideFeaturedTags />}
alwaysPrepend
append={remoteMessage}
emptyMessage={emptyMessage}

View file

@ -10,13 +10,19 @@ import {
fetchSubscribing,
expandSubscribing,
} from '../../actions/accounts';
import { FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import ColumnSettingsContainer from '../account_timeline/containers/column_settings_container';
import HeaderContainer from '../account_timeline/containers/header_container';
import ColumnBackButton from '../../components/column_back_button';
import ScrollableList from '../../components/scrollable_list';
import MissingIndicator from 'mastodon/components/missing_indicator';
import { new_features_policy } from 'mastodon/initial_state';
const messages = defineMessages({
title: { id: 'column.account', defaultMessage: 'Account' },
});
const mapStateToProps = (state, props) => ({
isAccount: !!state.getIn(['accounts', props.params.accountId]),
@ -24,15 +30,18 @@ const mapStateToProps = (state, props) => ({
hasMore: !!state.getIn(['user_lists', 'subscribing', props.params.accountId, 'next']),
isLoading: state.getIn(['user_lists', 'subscribing', props.params.accountId, 'isLoading'], true),
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
advancedMode: state.getIn(['settings', 'account', 'other', 'advancedMode'], new_features_policy === 'conservative' ? false : true),
});
export default @connect(mapStateToProps)
@injectIntl
class Subscribing extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
advancedMode: PropTypes.bool,
hasMore: PropTypes.bool,
blockedBy: PropTypes.bool,
isAccount: PropTypes.bool,
@ -57,8 +66,16 @@ class Subscribing extends ImmutablePureComponent {
this.props.dispatch(expandSubscribing(this.props.params.accountId));
}, 300, { leading: true });
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
render () {
const { accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading } = this.props;
const { accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, intl } = this.props;
if (!isAccount) {
return (
@ -79,15 +96,24 @@ class Subscribing extends ImmutablePureComponent {
const emptyMessage = blockedBy ? <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' /> : <FormattedMessage id='account.subscribes.empty' defaultMessage="This user doesn't subscribe anyone yet." />;
return (
<Column>
<ColumnBackButton multiColumn={multiColumn} />
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='user'
active={false}
title={intl.formatMessage(messages.title)}
onClick={this.handleHeaderClick}
pinned={false}
multiColumn={multiColumn}
>
<ColumnSettingsContainer />
</ColumnHeader>
<ScrollableList
scrollKey='subscribing'
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideProfile hideFeaturedTags />}
alwaysPrepend
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}

View file

@ -87,6 +87,8 @@ const mapStateToProps = state => ({
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
advancedMode: state.getIn(['settings', 'account', 'other', 'advancedMode'], false),
openPostsFirst: state.getIn(['settings', 'account', 'other', 'openPostsFirst'], false),
visibilities: getHomeVisibilities(state),
});
@ -129,6 +131,8 @@ class SwitchingColumnsArea extends React.PureComponent {
children: PropTypes.node,
location: PropTypes.object,
mobile: PropTypes.bool,
advancedMode: PropTypes.bool,
openPostsFirst: PropTypes.bool,
};
componentWillMount () {
@ -159,13 +163,15 @@ class SwitchingColumnsArea extends React.PureComponent {
}
render () {
const { children, mobile } = this.props;
const { children, mobile, advancedMode, openPostsFirst } = this.props;
const redirect = mobile ? <Redirect from='/' to='/timelines/home' exact /> : enableEmptyColumn ? <Redirect from='/' to='/empty' exact /> : <Redirect from='/' to='/getting-started' exact />;
const account_redirect = advancedMode && openPostsFirst ? <Redirect from='/accounts/:accountId' to='/accounts/:accountId/posts' exact /> : <Redirect from='/accounts/:accountId' to='/accounts/:accountId/about' exact />;
return (
<ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}>
<WrappedSwitch>
{redirect}
{account_redirect}
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
<WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
@ -200,7 +206,9 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/statuses/:statusId/mentions' component={Mentions} content={children} />
<WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} />
<WrappedRoute path='/accounts/:accountId/about' exact component={AccountTimeline} content={children} componentParams={{ about: true }} />
<WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
<WrappedRoute path='/accounts/:accountId/posts/:tagged?' component={AccountTimeline} content={children} componentParams={{ posts: true }} />
<WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} />
<WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} />
<WrappedRoute path='/accounts/:accountId/subscribing' component={Subscribing} content={children} />
@ -246,6 +254,8 @@ class UI extends React.PureComponent {
layout: PropTypes.string.isRequired,
firstLaunch: PropTypes.bool,
visibilities: PropTypes.arrayOf(PropTypes.string),
advancedMode: PropTypes.bool,
openPostsFirst: PropTypes.bool,
};
state = {
@ -534,7 +544,7 @@ class UI extends React.PureComponent {
render () {
const { draggingOver } = this.state;
const { children, isComposing, location, dropdownMenuIsOpen, layout } = this.props;
const { children, isComposing, location, dropdownMenuIsOpen, layout, advancedMode, openPostsFirst } = this.props;
const handlers = {
help: this.handleHotkeyToggleHelp,
@ -561,7 +571,7 @@ class UI extends React.PureComponent {
return (
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
<SwitchingColumnsArea location={location} mobile={layout === 'mobile' || layout === 'single-column'}>
<SwitchingColumnsArea location={location} mobile={layout === 'mobile' || layout === 'single-column'} advancedMode={advancedMode} openPostsFirst={openPostsFirst}>
{children}
</SwitchingColumnsArea>

View file

@ -11,6 +11,13 @@
"account.blocked": "Blocked",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Cancel follow request",
"account.column_settings.advanced_settings": "Advanced settings",
"account.column_settings.advanced_mode": "Advanced mode",
"account.column_settings.hide_featured_tags": "Hide featuread tags selection",
"account.column_settings.hide_relation": "Hide post and follow counters",
"account.column_settings.open_posts_first": "Open first post when selecting an account",
"account.column_settings.show_posts_in_about": "Show posts in about",
"account.column_settings.without_reblogs": "Without boosts",
"account.conversations": "Show conversations with @{name}",
"account.conversations_all": "Show all conversations",
"account.direct": "Direct message @{name}",
@ -27,6 +34,9 @@
"account.follows": "Follows",
"account.follows.empty": "This user doesn't follow anyone yet.",
"account.follows_you": "Follows you",
"account.hashtag_all": "All",
"account.hashtag_all_description": "All posts (deselect hashtags)",
"account.hashtag_select_description": "Select hashtag #{name}",
"account.hide_reblogs": "Hide boosts from @{name}",
"account.joined": "Joined",
"account.last_status": "Last active",
@ -49,6 +59,11 @@
"account.requested": "Awaiting approval. Click to cancel follow request",
"account.secret": "Secret",
"account.share": "Share @{name}'s profile",
"account.short.about": "About",
"account.short.conversations": "Conversations",
"account.short.media": "Media",
"account.short.posts": "Posts",
"account.short.with_replies": "Posts & Replies",
"account.show_reblogs": "Show boosts from @{name}",
"account.statuses_counter": "{count, plural, one {{counter} Post} other {{counter} Posts}}",
"account.subscribe": "Subscribe",
@ -64,6 +79,7 @@
"account.unmute_notifications": "Unmute notifications from @{name}",
"account_note.placeholder": "Click to add note",
"account_popup.more_users": "({number, plural, one {# other user} other {# other users}})",
"account.view_about": "View about",
"alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
"alert.rate_limited.title": "Rate limited",
"alert.unexpected.message": "An unexpected error occurred.",
@ -91,6 +107,7 @@
"circle.reply": "(Reply to circle context)",
"circle.select": "Select circle",
"circle.unselect": "(Select circle)",
"column.account": "Account",
"column.blocks": "Blocked users",
"column.bookmarks": "Bookmarks",
"column.circles": "Circles",
@ -223,6 +240,7 @@
"empty_column.blocks": "You haven't blocked any users yet.",
"empty_column.bookmarked_statuses": "You don't have any bookmarked posts yet. When you bookmark one, it will show up here.",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.conversation_unavailable": "No conversation with this user yet",
"empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
"empty_column.domain_blocks": "There are no blocked domains yet.",
"empty_column.emoji_reactioned_statuses": "You don't have any reaction posts yet. When you reaction one, it will show up here.",
@ -240,6 +258,7 @@
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
"empty_column.mutes": "You haven't muted any users yet.",
"empty_column.notifications": "You don't have any notifications yet. When other people interact with you, you will see it here.",
"empty_column.pinned_unavailable": "Pinned posts unavailable",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up",
"empty_column.referred_by_statuses": "There are no referred by posts yet. When someone refers a post, it will appear here.",
"empty_column.suggestions": "No one has suggestions yet.",

View file

@ -11,6 +11,13 @@
"account.blocked": "ブロック済み",
"account.browse_more_on_origin_server": "リモートで表示",
"account.cancel_follow_request": "フォローリクエストを取り消す",
"account.column_settings.advanced_settings": "詳細設定",
"account.column_settings.advanced_mode": "詳細モード",
"account.column_settings.hide_featured_tags": "注目のタグの選択を隠す",
"account.column_settings.hide_relation": "投稿とフォローのカウンターを隠す",
"account.column_settings.open_posts_first": "アカウント選択時、最初に投稿を開く",
"account.column_settings.show_posts_in_about": "概要に投稿を表示",
"account.column_settings.without_reblogs": "ブーストを除外",
"account.conversations": "@{name}さんとの会話を表示",
"account.conversations_all": "すべての会話を表示",
"account.direct": "@{name}さんにダイレクトメッセージ",
@ -27,6 +34,9 @@
"account.follows": "フォロー",
"account.follows.empty": "まだ誰もフォローしていません。",
"account.follows_you": "フォローされています",
"account.hashtag_all": "すべて",
"account.hashtag_all_description": "すべての投稿(ハッシュタグの選択解除)",
"account.hashtag_select_description": "ハッシュタグ #{name} を選択",
"account.hide_reblogs": "@{name}さんからのブーストを非表示",
"account.joined": "登録日",
"account.last_status": "最後の活動",
@ -49,6 +59,11 @@
"account.report": "@{name}さんを通報",
"account.requested": "フォロー承認待ちです。クリックしてキャンセル",
"account.share": "@{name}さんのプロフィールを共有する",
"account.short.about": "概要",
"account.short.conversations": "会話",
"account.short.media": "メディア",
"account.short.posts": "投稿",
"account.short.with_replies": "投稿と返信",
"account.show_reblogs": "@{name}さんからのブーストを表示",
"account.statuses_counter": "{counter} 投稿",
"account.subscribe": "購読",
@ -64,6 +79,7 @@
"account.unmute_notifications": "@{name}さんからの通知を受け取るようにする",
"account_note.placeholder": "クリックしてメモを追加",
"account_popup.more_users": "(他、{number}人のユーザー)",
"account.view_about": "概要を見る",
"alert.rate_limited.message": "{retry_time, time, medium} 以降に再度実行してください。",
"alert.rate_limited.title": "制限に達しました",
"alert.unexpected.message": "不明なエラーが発生しました。",
@ -91,6 +107,7 @@
"circles.new.title_placeholder": "新規サークル名",
"circles.search": "フォローされている人の中から検索",
"circles.subheading": "あなたのサークル",
"column.account": "アカウント",
"column.blocks": "ブロックしたユーザー",
"column.bookmarks": "ブックマーク",
"column.circles": "サークル",
@ -223,6 +240,7 @@
"empty_column.blocks": "まだ誰もブロックしていません。",
"empty_column.bookmarked_statuses": "まだ何もブックマーク登録していません。ブックマーク登録するとここに表示されます。",
"empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
"empty_column.conversation_unavailable": "このユーザーとの会話はまだありません。",
"empty_column.direct": "ダイレクトメッセージはまだありません。ダイレクトメッセージをやりとりすると、ここに表示されます。",
"empty_column.domain_blocks": "ブロックしているドメインはありません。",
"empty_column.emoji_reactioned_statuses": "まだ何も絵文字リアクションしていません。絵文字リアクションするとここに表示されます。",
@ -240,6 +258,7 @@
"empty_column.lists": "まだリストがありません。リストを作るとここに表示されます。",
"empty_column.mutes": "まだ誰もミュートしていません。",
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
"empty_column.pinned_unavailable": "固定された投稿はありません。",
"empty_column.referred_by_statuses": "まだ、参照している投稿はありません。誰かが投稿を参照すると、ここに表示されます。",
"empty_column.suggestions": "まだおすすめできるユーザーがいません。",
"empty_column.trends": "まだ何もトレンドがありません。",

View file

@ -16,6 +16,17 @@ const initialState = ImmutableMap({
show: true,
}),
account: ImmutableMap({
other: ImmutableMap({
advancedMode: true,
openPostsFirst: false,
withoutReblogs: false,
showPostsInAbout: true,
hideFeaturedTags: false,
hideRelation: false,
}),
}),
home: ImmutableMap({
shows: ImmutableMap({
reblog: true,

View file

@ -28,7 +28,7 @@ import {
FOLLOW_REQUESTS_EXPAND_FAIL,
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
FOLLOW_REQUEST_REJECT_SUCCESS,
} from '../actions/accounts';
} from '../actions/accounts';
import {
REBLOGS_FETCH_SUCCESS,
REBLOGS_FETCH_REQUEST,
@ -87,6 +87,11 @@ import {
DIRECTORY_EXPAND_SUCCESS,
DIRECTORY_EXPAND_FAIL,
} from 'mastodon/actions/directory';
import {
FEATURED_TAGS_FETCH_REQUEST,
FEATURED_TAGS_FETCH_SUCCESS,
FEATURED_TAGS_FETCH_FAIL,
} from 'mastodon/actions/featured_tags';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
const initialListState = ImmutableMap({
@ -106,6 +111,7 @@ const initialState = ImmutableMap({
follow_requests: initialListState,
blocks: initialListState,
mutes: initialListState,
featured_tags: initialListState,
});
const normalizeList = (state, path, accounts, next) => {
@ -147,6 +153,18 @@ const appendToEmojiReactions = (state, path, emojiReactions, next) => {
});
};
const normalizeFeaturedTag = (featuredTags, accountId) => {
const normalizeFeaturedTag = { ...featuredTags, accountId: accountId };
return fromJS(normalizeFeaturedTag);
};
const normalizeFeaturedTags = (state, path, featuredTags, accountId) => {
return state.setIn(path, ImmutableMap({
items: ImmutableList(featuredTags.map(featuredTag => normalizeFeaturedTag(featuredTag, accountId)).sort((a, b) => b.get('statuses_count') - a.get('statuses_count'))),
isLoading: false,
}));
};
export default function userLists(state = initialState, action) {
switch(action.type) {
case FOLLOWERS_FETCH_SUCCESS:
@ -274,6 +292,12 @@ export default function userLists(state = initialState, action) {
case DIRECTORY_FETCH_FAIL:
case DIRECTORY_EXPAND_FAIL:
return state.setIn(['directory', 'isLoading'], false);
case FEATURED_TAGS_FETCH_SUCCESS:
return normalizeFeaturedTags(state, ['featured_tags', action.id], action.tags, action.id);
case FEATURED_TAGS_FETCH_REQUEST:
return state.setIn(['featured_tags', action.id, 'isLoading'], true);
case FEATURED_TAGS_FETCH_FAIL:
return state.setIn(['featured_tags', action.id, 'isLoading'], false);
default:
return state;
}

View file

@ -6663,6 +6663,13 @@ a.status-card.compact:hover {
}
}
&.with-short-label {
button,
a {
padding: 10px 0;
}
}
&.directory__section-headline {
background: darken($ui-base-color, 2%);
border-bottom-color: transparent;
@ -6680,6 +6687,13 @@ a.status-card.compact:hover {
}
}
}
&__short-label {
font-size: 10px;
overflow-x: hidden;
white-space: nowrap;
display: block;
}
}
.filter-form {
@ -7569,6 +7583,51 @@ noscript {
}
}
}
&__hashtag-links {
overflow: hidden;
padding: 10px 5px;
margin: 0 -5px;
color: $darker-text-color;
border-bottom: 1px solid lighten($ui-base-color, 12%);
a {
display: inline-block;
color: $darker-text-color;
text-decoration: none;
padding: 5px 10px;
font-weight: 500;
strong {
font-weight: 700;
color: $primary-text-color;
}
}
a.active {
color: darken($ui-base-color, 4%);
background: $darker-text-color;
border-radius: 18px;
}
}
}
&.advanced {
.account__header__bio {
.account__header__personal--wrapper {
border-bottom: 1px solid lighten($ui-base-color, 12%);
}
}
.account__header__extra {
margin-top: 0;
padding: 0 5px;
.account__header__extra__links {
border-top: none;
border-bottom: 1px solid lighten($ui-base-color, 12%);
margin: 0 -5px;
}
}
}
&__account-note {