diff --git a/app/soapbox/actions/accounts.ts b/app/soapbox/actions/accounts.ts
index c3bc56557..a18367f1b 100644
--- a/app/soapbox/actions/accounts.ts
+++ b/app/soapbox/actions/accounts.ts
@@ -227,7 +227,12 @@ const fetchAccountFail = (id: string | null, error: AxiosError) => ({
skipAlert: true,
});
-const followAccount = (id: string, options = { reblogs: true }) =>
+type FollowAccountOpts = {
+ reblogs?: boolean,
+ notify?: boolean
+};
+
+const followAccount = (id: string, options?: FollowAccountOpts) =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return null;
diff --git a/app/soapbox/actions/reports.ts b/app/soapbox/actions/reports.ts
index 40b685ba4..dce162247 100644
--- a/app/soapbox/actions/reports.ts
+++ b/app/soapbox/actions/reports.ts
@@ -20,7 +20,7 @@ const REPORT_BLOCK_CHANGE = 'REPORT_BLOCK_CHANGE';
const REPORT_RULE_CHANGE = 'REPORT_RULE_CHANGE';
-const initReport = (account: Account, status: Status) =>
+const initReport = (account: Account, status?: Status) =>
(dispatch: AppDispatch) => {
dispatch({
type: REPORT_INIT,
@@ -121,4 +121,4 @@ export {
changeReportForward,
changeReportBlock,
changeReportRule,
-};
\ No newline at end of file
+};
diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js
deleted file mode 100644
index aaeed52ee..000000000
--- a/app/soapbox/features/account/components/header.js
+++ /dev/null
@@ -1,661 +0,0 @@
-'use strict';
-
-import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
-import debounce from 'lodash/debounce';
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
-import { connect } from 'react-redux';
-import { Link } from 'react-router-dom';
-
-import { openModal } from 'soapbox/actions/modals';
-import Avatar from 'soapbox/components/avatar';
-import Badge from 'soapbox/components/badge';
-import StillImage from 'soapbox/components/still_image';
-import { HStack, IconButton, Menu, MenuButton, MenuItem, MenuList, MenuLink, MenuDivider } from 'soapbox/components/ui';
-import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
-import ActionButton from 'soapbox/features/ui/components/action-button';
-import SubscriptionButton from 'soapbox/features/ui/components/subscription-button';
-import {
- isLocal,
- isRemote,
-} from 'soapbox/utils/accounts';
-import { getFeatures } from 'soapbox/utils/features';
-
-const messages = defineMessages({
- edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
- linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
- account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
- mention: { id: 'account.mention', defaultMessage: 'Mention' },
- chat: { id: 'account.chat', defaultMessage: 'Chat with @{name}' },
- direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' },
- unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
- block: { id: 'account.block', defaultMessage: 'Block @{name}' },
- unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{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: 'Hide everything from {domain}' },
- unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
- hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide reposts from @{name}' },
- showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show reposts from @{name}' },
- preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
- follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
- blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
- domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden 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' },
- removeFromFollowers: { id: 'account.remove_from_followers', defaultMessage: 'Remove this follower' },
- admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
- add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
- deactivateUser: { id: 'admin.users.actions.deactivate_user', defaultMessage: 'Deactivate @{name}' },
- deleteUser: { id: 'admin.users.actions.delete_user', defaultMessage: 'Delete @{name}' },
- verifyUser: { id: 'admin.users.actions.verify_user', defaultMessage: 'Verify @{name}' },
- unverifyUser: { id: 'admin.users.actions.unverify_user', defaultMessage: 'Unverify @{name}' },
- setDonor: { id: 'admin.users.actions.set_donor', defaultMessage: 'Set @{name} as a donor' },
- removeDonor: { id: 'admin.users.actions.remove_donor', defaultMessage: 'Remove @{name} as a donor' },
- promoteToAdmin: { id: 'admin.users.actions.promote_to_admin', defaultMessage: 'Promote @{name} to an admin' },
- promoteToModerator: { id: 'admin.users.actions.promote_to_moderator', defaultMessage: 'Promote @{name} to a moderator' },
- demoteToModerator: { id: 'admin.users.actions.demote_to_moderator', defaultMessage: 'Demote @{name} to a moderator' },
- demoteToUser: { id: 'admin.users.actions.demote_to_user', defaultMessage: 'Demote @{name} to a regular user' },
- suggestUser: { id: 'admin.users.actions.suggest_user', defaultMessage: 'Suggest @{name}' },
- unsuggestUser: { id: 'admin.users.actions.unsuggest_user', defaultMessage: 'Unsuggest @{name}' },
- search: { id: 'account.search', defaultMessage: 'Search from @{name}' },
-});
-
-const mapStateToProps = state => {
- const me = state.get('me');
- const account = state.getIn(['accounts', me]);
- const instance = state.get('instance');
- const features = getFeatures(instance);
-
- return {
- me,
- meAccount: account,
- features,
- };
-};
-
-export default @connect(mapStateToProps)
-@injectIntl
-class Header extends ImmutablePureComponent {
-
- static propTypes = {
- account: ImmutablePropTypes.record,
- meaccount: ImmutablePropTypes.record,
- intl: PropTypes.object.isRequired,
- username: PropTypes.string,
- features: PropTypes.object,
- };
-
- state = {
- isSmallScreen: (window.innerWidth <= 895),
- }
-
- isStatusesPageActive = (match, location) => {
- if (!match) {
- return false;
- }
-
- return !location.pathname.match(/\/(followers|following|favorites|pins)\/?$/);
- }
-
- componentDidMount() {
- window.addEventListener('resize', this.handleResize, { passive: true });
- }
-
- componentWillUnmount() {
- window.removeEventListener('resize', this.handleResize);
- }
-
- handleResize = debounce(() => {
- this.setState({ isSmallScreen: (window.innerWidth <= 895) });
- }, 5, {
- trailing: true,
- });
-
- onAvatarClick = () => {
- const avatar_url = this.props.account.get('avatar');
- const avatar = ImmutableMap({
- type: 'image',
- preview_url: avatar_url,
- url: avatar_url,
- description: '',
- });
- this.props.dispatch(openModal('MEDIA', { media: ImmutableList.of(avatar), index: 0 }));
- }
-
- handleAvatarClick = (e) => {
- if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
- e.preventDefault();
- this.onAvatarClick();
- }
- }
-
- onHeaderClick = () => {
- const header_url = this.props.account.get('header');
- const header = ImmutableMap({
- type: 'image',
- preview_url: header_url,
- url: header_url,
- description: '',
- });
- this.props.dispatch(openModal('MEDIA', { media: ImmutableList.of(header), index: 0 }));
- }
-
- handleHeaderClick = (e) => {
- if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
- e.preventDefault();
- this.onHeaderClick();
- }
- }
-
- handleShare = () => {
- navigator.share({
- text: `@${this.props.account.get('acct')}`,
- url: this.props.account.get('url'),
- }).catch((e) => {
- if (e.name !== 'AbortError') console.error(e);
- });
- }
-
- makeMenu() {
- const { account, intl, me, meAccount, features } = this.props;
-
- const menu = [];
-
- if (!account || !me) {
- return [];
- }
-
- if ('share' in navigator) {
- menu.push({
- text: intl.formatMessage(messages.share, { name: account.get('username') }),
- action: this.handleShare,
- icon: require('@tabler/icons/upload.svg'),
- });
- menu.push(null);
- }
-
- if (account.get('id') === me) {
- menu.push({
- text: intl.formatMessage(messages.edit_profile),
- to: '/settings/profile',
- icon: require('@tabler/icons/user.svg'),
- });
- menu.push({
- text: intl.formatMessage(messages.preferences),
- to: '/settings',
- icon: require('@tabler/icons/settings.svg'),
- });
- // menu.push(null);
- // menu.push({
- // text: intl.formatMessage(messages.follow_requests),
- // to: '/follow_requests',
- // icon: require('@tabler/icons/user-plus.svg'),
- // });
- menu.push(null);
- menu.push({
- text: intl.formatMessage(messages.mutes),
- to: '/mutes',
- icon: require('@tabler/icons/circle-x.svg'),
- });
- menu.push({
- text: intl.formatMessage(messages.blocks),
- to: '/blocks',
- icon: require('@tabler/icons/ban.svg'),
- });
- // menu.push({
- // text: intl.formatMessage(messages.domain_blocks),
- // to: '/domain_blocks',
- // icon: require('@tabler/icons/ban.svg'),
- // });
- } else {
- menu.push({
- text: intl.formatMessage(messages.mention, { name: account.get('username') }),
- action: this.props.onMention,
- icon: require('@tabler/icons/at.svg'),
- });
-
- // if (account.getIn(['pleroma', 'accepts_chat_messages'], false) === true) {
- // menu.push({
- // text: intl.formatMessage(messages.chat, { name: account.get('username') }),
- // action: this.props.onChat,
- // icon: require('@tabler/icons/messages.svg'),
- // });
- // } else {
- // menu.push({
- // text: intl.formatMessage(messages.direct, { name: account.get('username') }),
- // action: this.props.onDirect,
- // icon: require('@tabler/icons/mail.svg'),
- // });
- // }
-
- if (account.relationship?.following) {
- if (account.relationship?.showing_reblogs) {
- menu.push({
- text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }),
- action: this.props.onReblogToggle,
- icon: require('@tabler/icons/repeat.svg'),
- });
- } else {
- menu.push({
- text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }),
- action: this.props.onReblogToggle,
- icon: require('@tabler/icons/repeat.svg'),
- });
- }
-
- if (features.lists) {
- menu.push({
- text: intl.formatMessage(messages.add_or_remove_from_list),
- action: this.props.onAddToList,
- icon: require('@tabler/icons/list.svg'),
- });
- }
-
- if (features.accountEndorsements) {
- menu.push({
- text: intl.formatMessage(account.relationship?.endorsed ? messages.unendorse : messages.endorse),
- action: this.props.onEndorseToggle,
- icon: require('@tabler/icons/user-check.svg'),
- });
- }
-
- menu.push(null);
- } else if (features.lists && features.unrestrictedLists) {
- menu.push({
- text: intl.formatMessage(messages.add_or_remove_from_list),
- action: this.props.onAddToList,
- icon: require('@tabler/icons/list.svg'),
- });
- }
-
- if (features.searchFromAccount) {
- menu.push({
- text: intl.formatMessage(messages.search, { name: account.get('username') }),
- action: this.props.onSearch,
- icon: require('@tabler/icons/search.svg'),
- });
- }
-
- if (features.removeFromFollowers && account.relationship?.followed_by) {
- menu.push({
- text: intl.formatMessage(messages.removeFromFollowers),
- action: this.props.onRemoveFromFollowers,
- icon: require('@tabler/icons/user-x.svg'),
- });
- }
-
- if (account.relationship?.muting) {
- menu.push({
- text: intl.formatMessage(messages.unmute, { name: account.get('username') }),
- action: this.props.onMute,
- icon: require('@tabler/icons/circle-x.svg'),
- });
- } else {
- menu.push({
- text: intl.formatMessage(messages.mute, { name: account.get('username') }),
- action: this.props.onMute,
- icon: require('@tabler/icons/circle-x.svg'),
- });
- }
-
- if (account.relationship?.blocking) {
- menu.push({
- text: intl.formatMessage(messages.unblock, { name: account.get('username') }),
- action: this.props.onBlock,
- icon: require('@tabler/icons/ban.svg'),
- });
- } else {
- menu.push({
- text: intl.formatMessage(messages.block, { name: account.get('username') }),
- action: this.props.onBlock,
- icon: require('@tabler/icons/ban.svg'),
- });
- }
-
- menu.push({
- text: intl.formatMessage(messages.report, { name: account.get('username') }),
- action: this.props.onReport,
- icon: require('@tabler/icons/flag.svg'),
- });
- }
-
- if (isRemote(account)) {
- const domain = account.fqn.split('@')[1];
-
- menu.push(null);
-
- if (account.relationship?.domain_blocking) {
- menu.push({
- text: intl.formatMessage(messages.unblockDomain, { domain }),
- action: this.props.onUnblockDomain,
- icon: require('@tabler/icons/ban.svg'),
- });
- } else {
- menu.push({
- text: intl.formatMessage(messages.blockDomain, { domain }),
- action: this.props.onBlockDomain,
- icon: require('@tabler/icons/ban.svg'),
- });
- }
- }
-
- if (meAccount.staff) {
- menu.push(null);
-
- if (meAccount.admin) {
- menu.push({
- text: intl.formatMessage(messages.admin_account, { name: account.get('username') }),
- to: `/pleroma/admin/#/users/${account.id}/`,
- newTab: true,
- icon: require('@tabler/icons/gavel.svg'),
- });
- }
-
- if (account.id !== me && isLocal(account) && meAccount.admin) {
- if (account.admin) {
- menu.push({
- text: intl.formatMessage(messages.demoteToModerator, { name: account.get('username') }),
- action: this.props.onPromoteToModerator,
- icon: require('@tabler/icons/arrow-up-circle.svg'),
- });
- menu.push({
- text: intl.formatMessage(messages.demoteToUser, { name: account.get('username') }),
- action: this.props.onDemoteToUser,
- icon: require('@tabler/icons/arrow-down-circle.svg'),
- });
- } else if (account.moderator) {
- menu.push({
- text: intl.formatMessage(messages.promoteToAdmin, { name: account.get('username') }),
- action: this.props.onPromoteToAdmin,
- icon: require('@tabler/icons/arrow-up-circle.svg'),
- });
- menu.push({
- text: intl.formatMessage(messages.demoteToUser, { name: account.get('username') }),
- action: this.props.onDemoteToUser,
- icon: require('@tabler/icons/arrow-down-circle.svg'),
- });
- } else {
- menu.push({
- text: intl.formatMessage(messages.promoteToAdmin, { name: account.get('username') }),
- action: this.props.onPromoteToAdmin,
- icon: require('@tabler/icons/arrow-up-circle.svg'),
- });
- menu.push({
- text: intl.formatMessage(messages.promoteToModerator, { name: account.get('username') }),
- action: this.props.onPromoteToModerator,
- icon: require('@tabler/icons/arrow-up-circle.svg'),
- });
- }
- }
-
- if (account.verified) {
- menu.push({
- text: intl.formatMessage(messages.unverifyUser, { name: account.username }),
- action: this.props.onUnverifyUser,
- icon: require('@tabler/icons/check.svg'),
- });
- } else {
- menu.push({
- text: intl.formatMessage(messages.verifyUser, { name: account.username }),
- action: this.props.onVerifyUser,
- icon: require('@tabler/icons/check.svg'),
- });
- }
-
- if (account.donor) {
- menu.push({
- text: intl.formatMessage(messages.removeDonor, { name: account.username }),
- action: this.props.onRemoveDonor,
- icon: require('@tabler/icons/coin.svg'),
- });
- } else {
- menu.push({
- text: intl.formatMessage(messages.setDonor, { name: account.username }),
- action: this.props.onSetDonor,
- icon: require('@tabler/icons/coin.svg'),
- });
- }
-
- if (features.suggestionsV2 && meAccount.admin) {
- if (account.getIn(['pleroma', 'is_suggested'])) {
- menu.push({
- text: intl.formatMessage(messages.unsuggestUser, { name: account.get('username') }),
- action: this.props.onUnsuggestUser,
- icon: require('@tabler/icons/user-x.svg'),
- });
- } else {
- menu.push({
- text: intl.formatMessage(messages.suggestUser, { name: account.get('username') }),
- action: this.props.onSuggestUser,
- icon: require('@tabler/icons/user-check.svg'),
- });
- }
- }
-
- if (account.get('id') !== me) {
- menu.push({
- text: intl.formatMessage(messages.deactivateUser, { name: account.get('username') }),
- action: this.props.onDeactivateUser,
- icon: require('@tabler/icons/user-off.svg'),
- });
- menu.push({
- text: intl.formatMessage(messages.deleteUser, { name: account.get('username') }),
- icon: require('@tabler/icons/user-minus.svg'),
- });
- }
- }
-
- return menu;
- }
-
- makeInfo() {
- const { account, me } = this.props;
-
- const info = [];
-
- if (!account || !me) return info;
-
- if (me !== account.get('id') && account.relationship?.followed_by) {
- info.push(
- }
- />,
- );
- } else if (me !== account.get('id') && account.relationship?.blocking) {
- info.push(
- }
- />,
- );
- }
-
- if (me !== account.get('id') && account.relationship?.muting) {
- info.push(
- }
- />,
- );
- } else if (me !== account.get('id') && account.relationship?.domain_blocking) {
- info.push(
- }
- />,
- );
- }
-
- return info;
- }
-
- renderMessageButton() {
- const { intl, account, me } = this.props;
-
- if (!me || !account || account.get('id') === me) {
- return null;
- }
-
- const canChat = account.getIn(['pleroma', 'accepts_chat_messages'], false) === true;
-
- if (canChat) {
- return (
-
- );
- } else {
- return (
-
- );
- }
- }
-
- renderShareButton() {
- const { intl, account, me } = this.props;
- const canShare = 'share' in navigator;
-
- if (!(account && me && account.get('id') === me && canShare)) {
- return null;
- }
-
- return (
-
- );
- }
-
- render() {
- const { account, me } = this.props;
-
- if (!account) {
- return (
-
- );
- }
-
- const info = this.makeInfo();
- const menu = this.makeMenu();
- const header = account.get('header', '');
-
- return (
-
-
-
- {header && (
-
-
-
- )}
-
-
-
- {info}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {me && (
-
- )}
-
- {this.renderShareButton()}
- {/* {this.renderMessageButton()} */}
-
-
-
-
-
-
-
- );
- }
-
-}
diff --git a/app/soapbox/features/account/components/header.tsx b/app/soapbox/features/account/components/header.tsx
new file mode 100644
index 000000000..58b777e97
--- /dev/null
+++ b/app/soapbox/features/account/components/header.tsx
@@ -0,0 +1,815 @@
+'use strict';
+
+import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
+import React from 'react';
+import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
+import { Link, useHistory } from 'react-router-dom';
+
+import { blockAccount, followAccount, pinAccount, removeFromFollowers, unblockAccount, unmuteAccount, unpinAccount } from 'soapbox/actions/accounts';
+import { verifyUser, unverifyUser, setDonor, removeDonor, promoteToAdmin, promoteToModerator, demoteToUser, suggestUsers, unsuggestUsers } from 'soapbox/actions/admin';
+import { launchChat } from 'soapbox/actions/chats';
+import { mentionCompose, directCompose } from 'soapbox/actions/compose';
+import { blockDomain, unblockDomain } from 'soapbox/actions/domain_blocks';
+import { openModal } from 'soapbox/actions/modals';
+import { deactivateUserModal } from 'soapbox/actions/moderation';
+import { initMuteModal } from 'soapbox/actions/mutes';
+import { initReport } from 'soapbox/actions/reports';
+import { setSearchAccount } from 'soapbox/actions/search';
+import { getSettings } from 'soapbox/actions/settings';
+import snackbar from 'soapbox/actions/snackbar';
+import Avatar from 'soapbox/components/avatar';
+import Badge from 'soapbox/components/badge';
+import StillImage from 'soapbox/components/still_image';
+import { HStack, IconButton, Menu, MenuButton, MenuItem, MenuList, MenuLink, MenuDivider } from 'soapbox/components/ui';
+import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
+import MovedNote from 'soapbox/features/account_timeline/components/moved_note';
+import ActionButton from 'soapbox/features/ui/components/action-button';
+import SubscriptionButton from 'soapbox/features/ui/components/subscription-button';
+import { useAppDispatch, useFeatures, useOwnAccount } from 'soapbox/hooks';
+import { Account } from 'soapbox/types/entities';
+import {
+ isLocal,
+ isRemote,
+} from 'soapbox/utils/accounts';
+
+import type { Menu as MenuType } from 'soapbox/components/dropdown_menu';
+
+const messages = defineMessages({
+ edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
+ linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
+ account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
+ mention: { id: 'account.mention', defaultMessage: 'Mention' },
+ chat: { id: 'account.chat', defaultMessage: 'Chat with @{name}' },
+ direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' },
+ unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
+ block: { id: 'account.block', defaultMessage: 'Block @{name}' },
+ unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{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: 'Hide everything from {domain}' },
+ unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
+ hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide reposts from @{name}' },
+ showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show reposts from @{name}' },
+ preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
+ follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
+ blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
+ domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden 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' },
+ removeFromFollowers: { id: 'account.remove_from_followers', defaultMessage: 'Remove this follower' },
+ admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
+ add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
+ deactivateUser: { id: 'admin.users.actions.deactivate_user', defaultMessage: 'Deactivate @{name}' },
+ deleteUser: { id: 'admin.users.actions.delete_user', defaultMessage: 'Delete @{name}' },
+ verifyUser: { id: 'admin.users.actions.verify_user', defaultMessage: 'Verify @{name}' },
+ unverifyUser: { id: 'admin.users.actions.unverify_user', defaultMessage: 'Unverify @{name}' },
+ setDonor: { id: 'admin.users.actions.set_donor', defaultMessage: 'Set @{name} as a donor' },
+ removeDonor: { id: 'admin.users.actions.remove_donor', defaultMessage: 'Remove @{name} as a donor' },
+ promoteToAdmin: { id: 'admin.users.actions.promote_to_admin', defaultMessage: 'Promote @{name} to an admin' },
+ promoteToModerator: { id: 'admin.users.actions.promote_to_moderator', defaultMessage: 'Promote @{name} to a moderator' },
+ demoteToModerator: { id: 'admin.users.actions.demote_to_moderator', defaultMessage: 'Demote @{name} to a moderator' },
+ demoteToUser: { id: 'admin.users.actions.demote_to_user', defaultMessage: 'Demote @{name} to a regular user' },
+ suggestUser: { id: 'admin.users.actions.suggest_user', defaultMessage: 'Suggest @{name}' },
+ unsuggestUser: { id: 'admin.users.actions.unsuggest_user', defaultMessage: 'Unsuggest @{name}' },
+ search: { id: 'account.search', defaultMessage: 'Search from @{name}' },
+ unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
+ blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
+ blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
+ blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
+ userVerified: { id: 'admin.users.user_verified_message', defaultMessage: '@{acct} was verified' },
+ userUnverified: { id: 'admin.users.user_unverified_message', defaultMessage: '@{acct} was unverified' },
+ setDonorSuccess: { id: 'admin.users.set_donor_message', defaultMessage: '@{acct} was set as a donor' },
+ removeDonorSuccess: { id: 'admin.users.remove_donor_message', defaultMessage: '@{acct} was removed as a donor' },
+ promotedToAdmin: { id: 'admin.users.actions.promote_to_admin_message', defaultMessage: '@{acct} was promoted to an admin' },
+ promotedToModerator: { id: 'admin.users.actions.promote_to_moderator_message', defaultMessage: '@{acct} was promoted to a moderator' },
+ demotedToModerator: { id: 'admin.users.actions.demote_to_moderator_message', defaultMessage: '@{acct} was demoted to a moderator' },
+ demotedToUser: { id: 'admin.users.actions.demote_to_user_message', defaultMessage: '@{acct} was demoted to a regular user' },
+ userSuggested: { id: 'admin.users.user_suggested_message', defaultMessage: '@{acct} was suggested' },
+ userUnsuggested: { id: 'admin.users.user_unsuggested_message', defaultMessage: '@{acct} was unsuggested' },
+ removeFromFollowersConfirm: { id: 'confirmations.remove_from_followers.confirm', defaultMessage: 'Remove' },
+ userEndorsed: { id: 'account.endorse.success', defaultMessage: 'You are now featuring @{acct} on your profile' },
+ userUnendorsed: { id: 'account.unendorse.success', defaultMessage: 'You are no longer featuring @{acct}' },
+
+});
+
+interface IHeader {
+ account?: Account,
+}
+
+const Header: React.FC = ({ account }) => {
+ const intl = useIntl();
+ const history = useHistory();
+ const dispatch = useAppDispatch();
+
+ const features = useFeatures();
+ const ownAccount = useOwnAccount();
+
+ if (!account) {
+ return (
+
+ );
+ }
+
+ const onBlock = () => {
+ if (account.relationship?.blocking) {
+ dispatch(unblockAccount(account.id));
+ } else {
+ dispatch(openModal('CONFIRM', {
+ icon: require('@tabler/icons/ban.svg'),
+ heading: ,
+ message: @{account.acct} }} />,
+ confirm: intl.formatMessage(messages.blockConfirm),
+ onConfirm: () => dispatch(blockAccount(account.id)),
+ secondary: intl.formatMessage(messages.blockAndReport),
+ onSecondary: () => {
+ dispatch(blockAccount(account.id));
+ dispatch(initReport(account));
+ },
+ }));
+ }
+ };
+
+ const onMention = () => {
+ dispatch(mentionCompose(account));
+ };
+
+ const onDirect = () => {
+ dispatch(directCompose(account));
+ };
+
+ const onReblogToggle = () => {
+ if (account.relationship?.showing_reblogs) {
+ dispatch(followAccount(account.id, { reblogs: false }));
+ } else {
+ dispatch(followAccount(account.id, { reblogs: true }));
+ }
+ };
+
+ const onEndorseToggle = () => {
+ if (account.relationship?.endorsed) {
+ dispatch(unpinAccount(account.id))
+ .then(() => dispatch(snackbar.success(intl.formatMessage(messages.userUnendorsed, { acct: account.acct }))))
+ .catch(() => {});
+ } else {
+ dispatch(pinAccount(account.id))
+ .then(() => dispatch(snackbar.success(intl.formatMessage(messages.userEndorsed, { acct: account.acct }))))
+ .catch(() => {});
+ }
+ };
+
+ const onReport = () => {
+ dispatch(initReport(account));
+ };
+
+ const onMute = () => {
+ if (account.relationship?.muting) {
+ dispatch(unmuteAccount(account.id));
+ } else {
+ dispatch(initMuteModal(account));
+ }
+ };
+
+ const onBlockDomain = (domain: string) => {
+ dispatch(openModal('CONFIRM', {
+ icon: require('@tabler/icons/ban.svg'),
+ heading: ,
+ message: {domain} }} />,
+ confirm: intl.formatMessage(messages.blockDomainConfirm),
+ onConfirm: () => dispatch(blockDomain(domain)),
+ }));
+ };
+
+ const onUnblockDomain = (domain: string) => {
+ dispatch(unblockDomain(domain));
+ };
+
+ const onAddToList = () => {
+ dispatch(openModal('LIST_ADDER', {
+ accountId: account.id,
+ }));
+ };
+
+ const onChat = () => {
+ dispatch(launchChat(account.id, history));
+ };
+
+ const onDeactivateUser = () => {
+ dispatch(deactivateUserModal(intl, account.id));
+ };
+
+ const onVerifyUser = () => {
+ const message = intl.formatMessage(messages.userVerified, { acct: account.acct });
+
+ dispatch(verifyUser(account.id))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
+ };
+
+ const onUnverifyUser = () => {
+ const message = intl.formatMessage(messages.userUnverified, { acct: account.acct });
+
+ dispatch(unverifyUser(account.id))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
+ };
+
+ const onSetDonor = () => {
+ const message = intl.formatMessage(messages.setDonorSuccess, { acct: account.acct });
+
+ dispatch(setDonor(account.id))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
+ };
+
+ const onRemoveDonor = () => {
+ const message = intl.formatMessage(messages.removeDonorSuccess, { acct: account.acct });
+
+ dispatch(removeDonor(account.id))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
+ };
+
+ const onPromoteToAdmin = () => {
+ const message = intl.formatMessage(messages.promotedToAdmin, { acct: account.acct });
+
+ dispatch(promoteToAdmin(account.id))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
+ };
+
+ const onPromoteToModerator = () => {
+ const messageType = account.admin ? messages.demotedToModerator : messages.promotedToModerator;
+ const message = intl.formatMessage(messageType, { acct: account.acct });
+
+ dispatch(promoteToModerator(account.id))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
+ };
+
+ const onDemoteToUser = () => {
+ const message = intl.formatMessage(messages.demotedToUser, { acct: account.acct });
+
+ dispatch(demoteToUser(account.id))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
+ };
+
+ const onSuggestUser = () => {
+ const message = intl.formatMessage(messages.userSuggested, { acct: account.acct });
+
+ dispatch(suggestUsers([account.id]))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
+ };
+
+ const onUnsuggestUser = () => {
+ const message = intl.formatMessage(messages.userUnsuggested, { acct: account.acct });
+
+ dispatch(unsuggestUsers([account.id]))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
+ };
+
+ const onRemoveFromFollowers = () => {
+ dispatch((_, getState) => {
+ const unfollowModal = getSettings(getState()).get('unfollowModal');
+ if (unfollowModal) {
+ dispatch(openModal('CONFIRM', {
+ message: @{account.acct} }} />,
+ confirm: intl.formatMessage(messages.removeFromFollowersConfirm),
+ onConfirm: () => dispatch(removeFromFollowers(account.id)),
+ }));
+ } else {
+ dispatch(removeFromFollowers(account.id));
+ }
+ });
+ };
+
+ const onSearch = () => {
+ dispatch(setSearchAccount(account.id));
+ history.push('/search');
+ };
+
+ const onAvatarClick = () => {
+ const avatar_url = account.avatar;
+ const avatar = ImmutableMap({
+ type: 'image',
+ preview_url: avatar_url,
+ url: avatar_url,
+ description: '',
+ });
+ dispatch(openModal('MEDIA', { media: ImmutableList.of(avatar), index: 0 }));
+ };
+
+ const handleAvatarClick: React.MouseEventHandler = (e) => {
+ if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+ onAvatarClick();
+ }
+ };
+
+ const onHeaderClick = () => {
+ const header_url = account.header;
+ const header = ImmutableMap({
+ type: 'image',
+ preview_url: header_url,
+ url: header_url,
+ description: '',
+ });
+ dispatch(openModal('MEDIA', { media: ImmutableList.of(header), index: 0 }));
+ };
+
+ const handleHeaderClick: React.MouseEventHandler = (e) => {
+ if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+ onHeaderClick();
+ }
+ };
+
+ const handleShare = () => {
+ navigator.share({
+ text: `@${account.acct}`,
+ url: account.url,
+ }).catch((e) => {
+ if (e.name !== 'AbortError') console.error(e);
+ });
+ };
+
+ const makeMenu = () => {
+ const menu: MenuType = [];
+
+ if (!account || !ownAccount) {
+ return [];
+ }
+
+ if ('share' in navigator) {
+ menu.push({
+ text: intl.formatMessage(messages.share, { name: account.username }),
+ action: handleShare,
+ icon: require('@tabler/icons/upload.svg'),
+ });
+ menu.push(null);
+ }
+
+ if (account.id === ownAccount?.id) {
+ menu.push({
+ text: intl.formatMessage(messages.edit_profile),
+ to: '/settings/profile',
+ icon: require('@tabler/icons/user.svg'),
+ });
+ menu.push({
+ text: intl.formatMessage(messages.preferences),
+ to: '/settings',
+ icon: require('@tabler/icons/settings.svg'),
+ });
+ menu.push(null);
+ menu.push({
+ text: intl.formatMessage(messages.mutes),
+ to: '/mutes',
+ icon: require('@tabler/icons/circle-x.svg'),
+ });
+ menu.push({
+ text: intl.formatMessage(messages.blocks),
+ to: '/blocks',
+ icon: require('@tabler/icons/ban.svg'),
+ });
+ } else {
+ menu.push({
+ text: intl.formatMessage(messages.mention, { name: account.username }),
+ action: onMention,
+ icon: require('@tabler/icons/at.svg'),
+ });
+
+ if (account.getIn(['pleroma', 'accepts_chat_messages']) === true) {
+ menu.push({
+ text: intl.formatMessage(messages.chat, { name: account.username }),
+ action: onChat,
+ icon: require('@tabler/icons/messages.svg'),
+ });
+ } else if (features.privacyScopes) {
+ menu.push({
+ text: intl.formatMessage(messages.direct, { name: account.username }),
+ action: onDirect,
+ icon: require('@tabler/icons/mail.svg'),
+ });
+ }
+
+ if (account.relationship?.following) {
+ if (account.relationship?.showing_reblogs) {
+ menu.push({
+ text: intl.formatMessage(messages.hideReblogs, { name: account.username }),
+ action: onReblogToggle,
+ icon: require('@tabler/icons/repeat.svg'),
+ });
+ } else {
+ menu.push({
+ text: intl.formatMessage(messages.showReblogs, { name: account.username }),
+ action: onReblogToggle,
+ icon: require('@tabler/icons/repeat.svg'),
+ });
+ }
+
+ if (features.lists) {
+ menu.push({
+ text: intl.formatMessage(messages.add_or_remove_from_list),
+ action: onAddToList,
+ icon: require('@tabler/icons/list.svg'),
+ });
+ }
+
+ if (features.accountEndorsements) {
+ menu.push({
+ text: intl.formatMessage(account.relationship?.endorsed ? messages.unendorse : messages.endorse),
+ action: onEndorseToggle,
+ icon: require('@tabler/icons/user-check.svg'),
+ });
+ }
+
+ menu.push(null);
+ } else if (features.lists && features.unrestrictedLists) {
+ menu.push({
+ text: intl.formatMessage(messages.add_or_remove_from_list),
+ action: onAddToList,
+ icon: require('@tabler/icons/list.svg'),
+ });
+ }
+
+ if (features.searchFromAccount) {
+ menu.push({
+ text: intl.formatMessage(messages.search, { name: account.username }),
+ action: onSearch,
+ icon: require('@tabler/icons/search.svg'),
+ });
+ }
+
+ if (features.removeFromFollowers && account.relationship?.followed_by) {
+ menu.push({
+ text: intl.formatMessage(messages.removeFromFollowers),
+ action: onRemoveFromFollowers,
+ icon: require('@tabler/icons/user-x.svg'),
+ });
+ }
+
+ if (account.relationship?.muting) {
+ menu.push({
+ text: intl.formatMessage(messages.unmute, { name: account.username }),
+ action: onMute,
+ icon: require('@tabler/icons/circle-x.svg'),
+ });
+ } else {
+ menu.push({
+ text: intl.formatMessage(messages.mute, { name: account.username }),
+ action: onMute,
+ icon: require('@tabler/icons/circle-x.svg'),
+ });
+ }
+
+ if (account.relationship?.blocking) {
+ menu.push({
+ text: intl.formatMessage(messages.unblock, { name: account.username }),
+ action: onBlock,
+ icon: require('@tabler/icons/ban.svg'),
+ });
+ } else {
+ menu.push({
+ text: intl.formatMessage(messages.block, { name: account.username }),
+ action: onBlock,
+ icon: require('@tabler/icons/ban.svg'),
+ });
+ }
+
+ menu.push({
+ text: intl.formatMessage(messages.report, { name: account.username }),
+ action: onReport,
+ icon: require('@tabler/icons/flag.svg'),
+ });
+ }
+
+ if (isRemote(account)) {
+ const domain = account.fqn.split('@')[1];
+
+ menu.push(null);
+
+ if (account.relationship?.domain_blocking) {
+ menu.push({
+ text: intl.formatMessage(messages.unblockDomain, { domain }),
+ action: () => onUnblockDomain(domain),
+ icon: require('@tabler/icons/ban.svg'),
+ });
+ } else {
+ menu.push({
+ text: intl.formatMessage(messages.blockDomain, { domain }),
+ action: () => onBlockDomain(domain),
+ icon: require('@tabler/icons/ban.svg'),
+ });
+ }
+ }
+
+ if (ownAccount?.staff) {
+ menu.push(null);
+
+ if (ownAccount?.admin) {
+ menu.push({
+ text: intl.formatMessage(messages.admin_account, { name: account.username }),
+ to: `/pleroma/admin/#/users/${account.id}/`,
+ newTab: true,
+ icon: require('@tabler/icons/gavel.svg'),
+ });
+ }
+
+ if (account.id !== ownAccount?.id && isLocal(account) && ownAccount.admin) {
+ if (account.admin) {
+ menu.push({
+ text: intl.formatMessage(messages.demoteToModerator, { name: account.username }),
+ action: onPromoteToModerator,
+ icon: require('@tabler/icons/arrow-up-circle.svg'),
+ });
+ menu.push({
+ text: intl.formatMessage(messages.demoteToUser, { name: account.username }),
+ action: onDemoteToUser,
+ icon: require('@tabler/icons/arrow-down-circle.svg'),
+ });
+ } else if (account.moderator) {
+ menu.push({
+ text: intl.formatMessage(messages.promoteToAdmin, { name: account.username }),
+ action: onPromoteToAdmin,
+ icon: require('@tabler/icons/arrow-up-circle.svg'),
+ });
+ menu.push({
+ text: intl.formatMessage(messages.demoteToUser, { name: account.username }),
+ action: onDemoteToUser,
+ icon: require('@tabler/icons/arrow-down-circle.svg'),
+ });
+ } else {
+ menu.push({
+ text: intl.formatMessage(messages.promoteToAdmin, { name: account.username }),
+ action: onPromoteToAdmin,
+ icon: require('@tabler/icons/arrow-up-circle.svg'),
+ });
+ menu.push({
+ text: intl.formatMessage(messages.promoteToModerator, { name: account.username }),
+ action: onPromoteToModerator,
+ icon: require('@tabler/icons/arrow-up-circle.svg'),
+ });
+ }
+ }
+
+ if (account.verified) {
+ menu.push({
+ text: intl.formatMessage(messages.unverifyUser, { name: account.username }),
+ action: onUnverifyUser,
+ icon: require('@tabler/icons/check.svg'),
+ });
+ } else {
+ menu.push({
+ text: intl.formatMessage(messages.verifyUser, { name: account.username }),
+ action: onVerifyUser,
+ icon: require('@tabler/icons/check.svg'),
+ });
+ }
+
+ if (account.donor) {
+ menu.push({
+ text: intl.formatMessage(messages.removeDonor, { name: account.username }),
+ action: onRemoveDonor,
+ icon: require('@tabler/icons/coin.svg'),
+ });
+ } else {
+ menu.push({
+ text: intl.formatMessage(messages.setDonor, { name: account.username }),
+ action: onSetDonor,
+ icon: require('@tabler/icons/coin.svg'),
+ });
+ }
+
+ if (features.suggestionsV2 && ownAccount.admin) {
+ if (account.getIn(['pleroma', 'is_suggested'])) {
+ menu.push({
+ text: intl.formatMessage(messages.unsuggestUser, { name: account.username }),
+ action: onUnsuggestUser,
+ icon: require('@tabler/icons/user-x.svg'),
+ });
+ } else {
+ menu.push({
+ text: intl.formatMessage(messages.suggestUser, { name: account.username }),
+ action: onSuggestUser,
+ icon: require('@tabler/icons/user-check.svg'),
+ });
+ }
+ }
+
+ if (account.id !== ownAccount?.id) {
+ menu.push({
+ text: intl.formatMessage(messages.deactivateUser, { name: account.username }),
+ action: onDeactivateUser,
+ icon: require('@tabler/icons/user-off.svg'),
+ });
+ menu.push({
+ text: intl.formatMessage(messages.deleteUser, { name: account.username }),
+ icon: require('@tabler/icons/user-minus.svg'),
+ });
+ }
+ }
+
+ return menu;
+ };
+
+ const makeInfo = () => {
+ const info: React.ReactNode[] = [];
+
+ if (!account || !ownAccount) return info;
+
+ if (ownAccount?.id !== account.id && account.relationship?.followed_by) {
+ info.push(
+ }
+ />,
+ );
+ } else if (ownAccount?.id !== account.id && account.relationship?.blocking) {
+ info.push(
+ }
+ />,
+ );
+ }
+
+ if (ownAccount?.id !== account.id && account.relationship?.muting) {
+ info.push(
+ }
+ />,
+ );
+ } else if (ownAccount?.id !== account.id && account.relationship?.domain_blocking) {
+ info.push(
+ }
+ />,
+ );
+ }
+
+ return info;
+ };
+
+ // const renderMessageButton = () => {
+ // if (!ownAccount || !account || account.id === ownAccount?.id) {
+ // return null;
+ // }
+
+ // const canChat = account.getIn(['pleroma', 'accepts_chat_messages']) === true;
+
+ // if (canChat) {
+ // return (
+ //
+ // );
+ // } else {
+ // return (
+ //
+ // );
+ // }
+ // };
+
+ const renderShareButton = () => {
+ const canShare = 'share' in navigator;
+
+ if (!(account && ownAccount?.id && account.id === ownAccount?.id && canShare)) {
+ return null;
+ }
+
+ return (
+
+ );
+ };
+
+ const info = makeInfo();
+ const menu = makeMenu();
+
+ return (
+
+ {(account.moved && typeof account.moved === 'object') && (
+
+ )}
+
+
+
+ {account.header && (
+
+
+
+ )}
+
+
+
+ {info}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {ownAccount && (
+
+ )}
+
+ {renderShareButton()}
+ {/* {renderMessageButton()} */}
+
+
+
+
+
+
+
+ );
+};
+
+export default Header;
diff --git a/app/soapbox/features/account_gallery/components/media_item.js b/app/soapbox/features/account_gallery/components/media_item.js
deleted file mode 100644
index fe8c6cc85..000000000
--- a/app/soapbox/features/account_gallery/components/media_item.js
+++ /dev/null
@@ -1,158 +0,0 @@
-import classNames from 'classnames';
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
-
-import { getSettings } from 'soapbox/actions/settings';
-import Blurhash from 'soapbox/components/blurhash';
-import Icon from 'soapbox/components/icon';
-import StillImage from 'soapbox/components/still_image';
-import { isIOS } from 'soapbox/is_mobile';
-
-const mapStateToProps = state => ({
- autoPlayGif: getSettings(state).get('autoPlayGif'),
- displayMedia: getSettings(state).get('displayMedia'),
-});
-
-export default @connect(mapStateToProps)
-class MediaItem extends ImmutablePureComponent {
-
- static propTypes = {
- attachment: ImmutablePropTypes.map.isRequired,
- displayWidth: PropTypes.number.isRequired,
- onOpenMedia: PropTypes.func.isRequired,
- autoPlayGif: PropTypes.bool,
- displayMedia: PropTypes.string,
- };
-
- state = {
- visible: this.props.displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || this.props.displayMedia === 'show_all',
- loaded: false,
- };
-
- handleImageLoad = () => {
- this.setState({ loaded: true });
- }
-
- handleMouseEnter = e => {
- if (this.hoverToPlay()) {
- e.target.play();
- }
- }
-
- handleMouseLeave = e => {
- if (this.hoverToPlay()) {
- e.target.pause();
- e.target.currentTime = 0;
- }
- }
-
- hoverToPlay = () => {
- const { autoPlayGif } = this.props;
- return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1;
- }
-
- handleClick = e => {
- if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
- e.preventDefault();
-
- if (this.state.visible) {
- this.props.onOpenMedia(this.props.attachment);
- } else {
- this.setState({ visible: true });
- }
- }
- }
-
- render() {
- const { attachment, displayWidth, autoPlayGif } = this.props;
- const { visible, loaded } = this.state;
-
- const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
- const height = width;
- const status = attachment.get('status');
- const title = status.get('spoiler_text') || attachment.get('description');
-
- let thumbnail = '';
- let icon;
-
- if (attachment.get('type') === 'unknown') {
- // Skip
- } else if (attachment.get('type') === 'image') {
- const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
- const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
- const x = ((focusX / 2) + .5) * 100;
- const y = ((focusY / -2) + .5) * 100;
-
- thumbnail = (
-
- );
- } else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) {
- const conditionalAttributes = {};
- if (isIOS()) {
- conditionalAttributes.playsInline = '1';
- }
- if (autoPlayGif) {
- conditionalAttributes.autoPlay = '1';
- }
- thumbnail = (
-
-
-
- GIF
-
- );
- } else if (attachment.get('type') === 'audio') {
- const remoteURL = attachment.get('remote_url') || '';
- const fileExtensionLastIndex = remoteURL.lastIndexOf('.');
- const fileExtension = remoteURL.substr(fileExtensionLastIndex + 1).toUpperCase();
- thumbnail = (
-
-
- {fileExtension}
-
- );
- }
-
- if (!visible) {
- icon = (
-
-
-
- );
- }
-
- return (
-
- );
- }
-
-}
diff --git a/app/soapbox/features/account_gallery/components/media_item.tsx b/app/soapbox/features/account_gallery/components/media_item.tsx
new file mode 100644
index 000000000..c113ac5e6
--- /dev/null
+++ b/app/soapbox/features/account_gallery/components/media_item.tsx
@@ -0,0 +1,141 @@
+import classNames from 'classnames';
+import React, { useState } from 'react';
+
+import Blurhash from 'soapbox/components/blurhash';
+import Icon from 'soapbox/components/icon';
+import StillImage from 'soapbox/components/still_image';
+import { useSettings } from 'soapbox/hooks';
+import { isIOS } from 'soapbox/is_mobile';
+
+import type { Attachment } from 'soapbox/types/entities';
+
+interface IMediaItem {
+ attachment: Attachment,
+ displayWidth: number,
+ onOpenMedia: (attachment: Attachment) => void,
+}
+
+const MediaItem: React.FC = ({ attachment, displayWidth, onOpenMedia }) => {
+ const settings = useSettings();
+ const autoPlayGif = settings.get('autoPlayGif');
+ const displayMedia = settings.get('displayMedia');
+
+ const [visible, setVisible] = useState(displayMedia !== 'hide_all' && !attachment.status?.sensitive || displayMedia === 'show_all');
+
+ const handleMouseEnter: React.MouseEventHandler = e => {
+ const video = e.target as HTMLVideoElement;
+ if (hoverToPlay()) {
+ video.play();
+ }
+ };
+
+ const handleMouseLeave: React.MouseEventHandler = e => {
+ const video = e.target as HTMLVideoElement;
+ if (hoverToPlay()) {
+ video.pause();
+ video.currentTime = 0;
+ }
+ };
+
+ const hoverToPlay = () => {
+ return !autoPlayGif && ['gifv', 'video'].indexOf(attachment.type) !== -1;
+ };
+
+ const handleClick: React.MouseEventHandler = e => {
+ if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+
+ if (visible) {
+ onOpenMedia(attachment);
+ } else {
+ setVisible(true);
+ }
+ }
+ };
+
+ const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`;
+ const height = width;
+ const status = attachment.get('status');
+ const title = status.get('spoiler_text') || attachment.get('description');
+
+ let thumbnail: React.ReactNode = '';
+ let icon;
+
+ if (attachment.type === 'unknown') {
+ // Skip
+ } else if (attachment.type === 'image') {
+ const focusX = Number(attachment.getIn(['meta', 'focus', 'x'])) || 0;
+ const focusY = Number(attachment.getIn(['meta', 'focus', 'y'])) || 0;
+ const x = ((focusX / 2) + .5) * 100;
+ const y = ((focusY / -2) + .5) * 100;
+
+ thumbnail = (
+
+ );
+ } else if (['gifv', 'video'].indexOf(attachment.type) !== -1) {
+ const conditionalAttributes: React.VideoHTMLAttributes = {};
+ if (isIOS()) {
+ conditionalAttributes.playsInline = true;
+ }
+ if (autoPlayGif) {
+ conditionalAttributes.autoPlay = true;
+ }
+ thumbnail = (
+
+
+
+ GIF
+
+ );
+ } else if (attachment.type === 'audio') {
+ const remoteURL = attachment.remote_url || '';
+ const fileExtensionLastIndex = remoteURL.lastIndexOf('.');
+ const fileExtension = remoteURL.substr(fileExtensionLastIndex + 1).toUpperCase();
+ thumbnail = (
+
+
+ {fileExtension}
+
+ );
+ }
+
+ if (!visible) {
+ icon = (
+
+
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default MediaItem;
diff --git a/app/soapbox/features/account_timeline/components/column_settings.js b/app/soapbox/features/account_timeline/components/column_settings.js
deleted file mode 100644
index 236fb4583..000000000
--- a/app/soapbox/features/account_timeline/components/column_settings.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
-
-import IconButton from 'soapbox/components/icon_button';
-
-import SettingToggle from '../../notifications/components/setting_toggle';
-
-const messages = defineMessages({
- close: { id: 'lightbox.close', defaultMessage: 'Close' },
-});
-
-export default @injectIntl
-class ColumnSettings extends React.PureComponent {
-
- static propTypes = {
- intl: PropTypes.object.isRequired,
- settings: ImmutablePropTypes.map.isRequired,
- onChange: PropTypes.func.isRequired,
- onClose: PropTypes.func.isRequired,
- };
-
- render() {
- const { intl, settings, onChange, onClose } = this.props;
-
- return (
-
-
-
-
-
-
-
-
-
- }
- />
- }
- />
-
-
-
- );
- }
-
-}
diff --git a/app/soapbox/features/account_timeline/components/header.js b/app/soapbox/features/account_timeline/components/header.js
deleted file mode 100644
index 493bd7a39..000000000
--- a/app/soapbox/features/account_timeline/components/header.js
+++ /dev/null
@@ -1,198 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { withRouter } from 'react-router-dom';
-
-import InnerHeader from '../../account/components/header';
-
-import MovedNote from './moved_note';
-
-export default @withRouter
-class Header extends ImmutablePureComponent {
-
- static propTypes = {
- account: ImmutablePropTypes.record,
- onFollow: PropTypes.func.isRequired,
- onBlock: PropTypes.func.isRequired,
- onMention: PropTypes.func.isRequired,
- onDirect: PropTypes.func.isRequired,
- onChat: PropTypes.func,
- onReblogToggle: 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,
- onRemoveFromFollowers: PropTypes.func.isRequired,
- onSearch: PropTypes.func.isRequired,
- username: PropTypes.string,
- history: PropTypes.object,
- };
-
- handleFollow = () => {
- this.props.onFollow(this.props.account);
- }
-
- handleBlock = () => {
- this.props.onBlock(this.props.account);
- }
-
- handleMention = () => {
- this.props.onMention(this.props.account);
- }
-
- handleDirect = () => {
- this.props.onDirect(this.props.account);
- }
-
- handleReport = () => {
- this.props.onReport(this.props.account);
- }
-
- handleReblogToggle = () => {
- this.props.onReblogToggle(this.props.account);
- }
-
- handleSubscriptionToggle = () => {
- this.props.onSubscriptionToggle(this.props.account);
- }
-
- handleNotifyToggle = () => {
- this.props.onNotifyToggle(this.props.account);
- }
-
- handleMute = () => {
- this.props.onMute(this.props.account);
- }
-
- handleBlockDomain = () => {
- const domain = this.props.account.get('acct').split('@')[1];
-
- if (!domain) return;
-
- this.props.onBlockDomain(domain);
- }
-
- handleUnblockDomain = () => {
- const domain = this.props.account.get('acct').split('@')[1];
-
- if (!domain) return;
-
- this.props.onUnblockDomain(domain);
- }
-
- handleChat = () => {
- this.props.onChat(this.props.account, this.props.history);
- }
-
- handleEndorseToggle = () => {
- this.props.onEndorseToggle(this.props.account);
- }
-
- handleAddToList = () => {
- this.props.onAddToList(this.props.account);
- }
-
- handleDeactivateUser = () => {
- this.props.onDeactivateUser(this.props.account);
- }
-
- handleDeleteUser = () => {
- this.props.onDeleteUser(this.props.account);
- }
-
- handleVerifyUser = () => {
- this.props.onVerifyUser(this.props.account);
- }
-
- handleUnverifyUser = () => {
- this.props.onUnverifyUser(this.props.account);
- }
-
- handleSetDonor = () => {
- this.props.onSetDonor(this.props.account);
- }
-
- handleRemoveDonor = () => {
- this.props.onRemoveDonor(this.props.account);
- }
-
- handlePromoteToAdmin = () => {
- this.props.onPromoteToAdmin(this.props.account);
- }
-
- handlePromoteToModerator = () => {
- this.props.onPromoteToModerator(this.props.account);
- }
-
- handleDemoteToUser = () => {
- this.props.onDemoteToUser(this.props.account);
- }
-
- handleSuggestUser = () => {
- this.props.onSuggestUser(this.props.account);
- }
-
- handleUnsuggestUser = () => {
- this.props.onUnsuggestUser(this.props.account);
- }
-
- handleShowNote = () => {
- this.props.onShowNote(this.props.account);
- }
-
- handleRemoveFromFollowers = () => {
- this.props.onRemoveFromFollowers(this.props.account);
- }
-
- handleSearch = () => {
- this.props.onSearch(this.props.account, this.props.history);
- }
-
- render() {
- const { account } = this.props;
- const moved = (account) ? account.get('moved') : false;
-
- return (
- <>
- { moved && }
-
-
- >
- );
- }
-
-}
diff --git a/app/soapbox/features/account_timeline/containers/column_settings_container.js b/app/soapbox/features/account_timeline/containers/column_settings_container.js
deleted file mode 100644
index c0b9fdf35..000000000
--- a/app/soapbox/features/account_timeline/containers/column_settings_container.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import { connect } from 'react-redux';
-
-import { getSettings, changeSetting } from '../../../actions/settings';
-import ColumnSettings from '../components/column_settings';
-
-const mapStateToProps = state => ({
- settings: getSettings(state).get('account_timeline'),
-});
-
-const mapDispatchToProps = (dispatch) => {
- return {
- onChange(key, checked) {
- dispatch(changeSetting(['account_timeline', ...key], checked));
- },
- };
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
diff --git a/app/soapbox/features/account_timeline/containers/header_container.js b/app/soapbox/features/account_timeline/containers/header_container.js
deleted file mode 100644
index af113d8f2..000000000
--- a/app/soapbox/features/account_timeline/containers/header_container.js
+++ /dev/null
@@ -1,304 +0,0 @@
-import React from 'react';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { connect } from 'react-redux';
-
-import { initAccountNoteModal } from 'soapbox/actions/account-notes';
-import {
- followAccount,
- unfollowAccount,
- blockAccount,
- unblockAccount,
- unmuteAccount,
- pinAccount,
- unpinAccount,
- subscribeAccount,
- unsubscribeAccount,
- removeFromFollowers,
-} from 'soapbox/actions/accounts';
-import {
- verifyUser,
- unverifyUser,
- setDonor,
- removeDonor,
- promoteToAdmin,
- promoteToModerator,
- demoteToUser,
- suggestUsers,
- unsuggestUsers,
-} from 'soapbox/actions/admin';
-import { launchChat } from 'soapbox/actions/chats';
-import {
- mentionCompose,
- directCompose,
-} from 'soapbox/actions/compose';
-import { blockDomain, unblockDomain } from 'soapbox/actions/domain_blocks';
-import { openModal } from 'soapbox/actions/modals';
-import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
-import { initMuteModal } from 'soapbox/actions/mutes';
-import { initReport } from 'soapbox/actions/reports';
-import { setSearchAccount } from 'soapbox/actions/search';
-import { getSettings } from 'soapbox/actions/settings';
-import snackbar from 'soapbox/actions/snackbar';
-import { makeGetAccount } from 'soapbox/selectors';
-
-import Header from '../components/header';
-
-const messages = defineMessages({
- unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
- blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
- blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
- blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
- userVerified: { id: 'admin.users.user_verified_message', defaultMessage: '@{acct} was verified' },
- userUnverified: { id: 'admin.users.user_unverified_message', defaultMessage: '@{acct} was unverified' },
- setDonor: { id: 'admin.users.set_donor_message', defaultMessage: '@{acct} was set as a donor' },
- removeDonor: { id: 'admin.users.remove_donor_message', defaultMessage: '@{acct} was removed as a donor' },
- promotedToAdmin: { id: 'admin.users.actions.promote_to_admin_message', defaultMessage: '@{acct} was promoted to an admin' },
- promotedToModerator: { id: 'admin.users.actions.promote_to_moderator_message', defaultMessage: '@{acct} was promoted to a moderator' },
- demotedToModerator: { id: 'admin.users.actions.demote_to_moderator_message', defaultMessage: '@{acct} was demoted to a moderator' },
- demotedToUser: { id: 'admin.users.actions.demote_to_user_message', defaultMessage: '@{acct} was demoted to a regular user' },
- userSuggested: { id: 'admin.users.user_suggested_message', defaultMessage: '@{acct} was suggested' },
- userUnsuggested: { id: 'admin.users.user_unsuggested_message', defaultMessage: '@{acct} was unsuggested' },
- removeFromFollowersConfirm: { id: 'confirmations.remove_from_followers.confirm', defaultMessage: 'Remove' },
- userEndorsed: { id: 'account.endorse.success', defaultMessage: 'You are now featuring @{acct} on your profile' },
- userUnendorsed: { id: 'account.unendorse.success', defaultMessage: 'You are no longer featuring @{acct}' },
-});
-
-const makeMapStateToProps = () => {
- const getAccount = makeGetAccount();
-
- const mapStateToProps = (state, { accountId }) => ({
- account: getAccount(state, accountId),
- });
-
- return mapStateToProps;
-};
-
-const mapDispatchToProps = (dispatch, { intl }) => ({
-
- onFollow(account) {
- dispatch((_, getState) => {
- const unfollowModal = getSettings(getState()).get('unfollowModal');
- if (account.relationship?.following || account.relationship?.requested) {
- if (unfollowModal) {
- dispatch(openModal('CONFIRM', {
- message: @{account.get('acct')} }} />,
- confirm: intl.formatMessage(messages.unfollowConfirm),
- onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
- }));
- } else {
- dispatch(unfollowAccount(account.get('id')));
- }
- } else {
- dispatch(followAccount(account.get('id')));
- }
- });
- },
-
- onBlock(account) {
- if (account.relationship?.blocking) {
- dispatch(unblockAccount(account.get('id')));
- } else {
- dispatch(openModal('CONFIRM', {
- icon: require('@tabler/icons/ban.svg'),
- heading: ,
- message: @{account.get('acct')} }} />,
- confirm: intl.formatMessage(messages.blockConfirm),
- onConfirm: () => dispatch(blockAccount(account.get('id'))),
- secondary: intl.formatMessage(messages.blockAndReport),
- onSecondary: () => {
- dispatch(blockAccount(account.get('id')));
- dispatch(initReport(account));
- },
- }));
- }
- },
-
- onMention(account) {
- dispatch(mentionCompose(account));
- },
-
- onDirect(account) {
- dispatch(directCompose(account));
- },
-
- onReblogToggle(account) {
- if (account.relationship?.showing_reblogs) {
- dispatch(followAccount(account.get('id'), { reblogs: false }));
- } else {
- dispatch(followAccount(account.get('id'), { reblogs: true }));
- }
- },
-
- onSubscriptionToggle(account) {
- if (account.relationship?.subscribing) {
- dispatch(unsubscribeAccount(account.get('id')));
- } else {
- dispatch(subscribeAccount(account.get('id')));
- }
- },
-
- onNotifyToggle(account) {
- if (account.relationship?.notifying) {
- dispatch(followAccount(account.get('id'), { notify: false }));
- } else {
- dispatch(followAccount(account.get('id'), { notify: true }));
- }
- },
-
- onEndorseToggle(account) {
- if (account.relationship?.endorsed) {
- dispatch(unpinAccount(account.get('id')))
- .then(() => dispatch(snackbar.success(intl.formatMessage(messages.userUnendorsed, { acct: account.acct }))))
- .catch(() => {});
- } else {
- dispatch(pinAccount(account.get('id')))
- .then(() => dispatch(snackbar.success(intl.formatMessage(messages.userEndorsed, { acct: account.acct }))))
- .catch(() => {});
- }
- },
-
- onReport(account) {
- dispatch(initReport(account));
- },
-
- onMute(account) {
- if (account.relationship?.muting) {
- dispatch(unmuteAccount(account.get('id')));
- } else {
- dispatch(initMuteModal(account));
- }
- },
-
- onBlockDomain(domain) {
- dispatch(openModal('CONFIRM', {
- icon: require('@tabler/icons/ban.svg'),
- heading: ,
- message: {domain} }} />,
- confirm: intl.formatMessage(messages.blockDomainConfirm),
- onConfirm: () => dispatch(blockDomain(domain)),
- }));
- },
-
- onUnblockDomain(domain) {
- dispatch(unblockDomain(domain));
- },
-
- onAddToList(account) {
- dispatch(openModal('LIST_ADDER', {
- accountId: account.get('id'),
- }));
- },
-
- onChat(account, router) {
- dispatch(launchChat(account.get('id'), router));
- },
-
- onDeactivateUser(account) {
- dispatch(deactivateUserModal(intl, account.get('id')));
- },
-
- onDeleteUser(account) {
- dispatch(deleteUserModal(intl, account.get('id')));
- },
-
- onVerifyUser(account) {
- const message = intl.formatMessage(messages.userVerified, { acct: account.get('acct') });
-
- dispatch(verifyUser(account.get('id')))
- .then(() => dispatch(snackbar.success(message)))
- .catch(() => {});
- },
-
- onUnverifyUser(account) {
- const message = intl.formatMessage(messages.userUnverified, { acct: account.get('acct') });
-
- dispatch(unverifyUser(account.get('id')))
- .then(() => dispatch(snackbar.success(message)))
- .catch(() => {});
- },
-
- onSetDonor(account) {
- const message = intl.formatMessage(messages.setDonor, { acct: account.get('acct') });
-
- dispatch(setDonor(account.get('id')))
- .then(() => dispatch(snackbar.success(message)))
- .catch(() => {});
- },
-
- onRemoveDonor(account) {
- const message = intl.formatMessage(messages.removeDonor, { acct: account.get('acct') });
-
- dispatch(removeDonor(account.get('id')))
- .then(() => dispatch(snackbar.success(message)))
- .catch(() => {});
- },
-
- onPromoteToAdmin(account) {
- const message = intl.formatMessage(messages.promotedToAdmin, { acct: account.get('acct') });
-
- dispatch(promoteToAdmin(account.get('id')))
- .then(() => dispatch(snackbar.success(message)))
- .catch(() => {});
- },
-
- onPromoteToModerator(account) {
- const messageType = account.admin ? messages.demotedToModerator : messages.promotedToModerator;
- const message = intl.formatMessage(messageType, { acct: account.get('acct') });
-
- dispatch(promoteToModerator(account.get('id')))
- .then(() => dispatch(snackbar.success(message)))
- .catch(() => {});
- },
-
- onDemoteToUser(account) {
- const message = intl.formatMessage(messages.demotedToUser, { acct: account.get('acct') });
-
- dispatch(demoteToUser(account.get('id')))
- .then(() => dispatch(snackbar.success(message)))
- .catch(() => {});
- },
-
- onSuggestUser(account) {
- const message = intl.formatMessage(messages.userSuggested, { acct: account.get('acct') });
-
- dispatch(suggestUsers([account.get('id')]))
- .then(() => dispatch(snackbar.success(message)))
- .catch(() => {});
- },
-
- onUnsuggestUser(account) {
- const message = intl.formatMessage(messages.userUnsuggested, { acct: account.get('acct') });
-
- dispatch(unsuggestUsers([account.get('id')]))
- .then(() => dispatch(snackbar.success(message)))
- .catch(() => {});
- },
-
- onShowNote(account) {
- dispatch(initAccountNoteModal(account));
- },
-
- onRemoveFromFollowers(account) {
- dispatch((_, getState) => {
- const unfollowModal = getSettings(getState()).get('unfollowModal');
- if (unfollowModal) {
- dispatch(openModal('CONFIRM', {
- message: @{account.get('acct')} }} />,
- confirm: intl.formatMessage(messages.removeFromFollowersConfirm),
- onConfirm: () => dispatch(removeFromFollowers(account.get('id'))),
- }));
- } else {
- dispatch(removeFromFollowers(account.get('id')));
- }
- });
- },
-
- onSearch(account, router) {
- dispatch((dispatch) => {
- dispatch(setSearchAccount(account.id));
- router.push('/search');
- });
- },
-});
-
-export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
diff --git a/app/soapbox/features/account_timeline/index.js b/app/soapbox/features/account_timeline/index.js
deleted file mode 100644
index 907c54d68..000000000
--- a/app/soapbox/features/account_timeline/index.js
+++ /dev/null
@@ -1,174 +0,0 @@
-import { OrderedSet as ImmutableOrderedSet } from 'immutable';
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { FormattedMessage } from 'react-intl';
-import { connect } from 'react-redux';
-import { withRouter } from 'react-router-dom';
-
-import { fetchAccountByUsername } from 'soapbox/actions/accounts';
-import { fetchPatronAccount } from 'soapbox/actions/patron';
-import { getSettings } from 'soapbox/actions/settings';
-import { getSoapboxConfig } from 'soapbox/actions/soapbox';
-import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'soapbox/actions/timelines';
-import MissingIndicator from 'soapbox/components/missing_indicator';
-import StatusList from 'soapbox/components/status_list';
-import { Card, CardBody, Spinner, Text } from 'soapbox/components/ui';
-import { makeGetStatusIds, findAccountByUsername } from 'soapbox/selectors';
-import { getFeatures } from 'soapbox/utils/features';
-
-const makeMapStateToProps = () => {
- const getStatusIds = makeGetStatusIds();
-
- const mapStateToProps = (state, { params, withReplies = false }) => {
- const username = params.username || '';
- const me = state.get('me');
- const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase());
- const soapboxConfig = getSoapboxConfig(state);
- const features = getFeatures(state.get('instance'));
-
- let accountId = -1;
- let account = null;
- let accountUsername = username;
- let accountApId = null;
- if (accountFetchError) {
- accountId = null;
- } else {
- account = findAccountByUsername(state, username);
- accountId = account ? account.getIn(['id'], null) : -1;
- accountUsername = account ? account.getIn(['acct'], '') : '';
- accountApId = account ? account.get('url') : '';
- }
-
- const path = withReplies ? `${accountId}:with_replies` : accountId;
-
- const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
- const unavailable = (me === accountId) ? false : (isBlocked && !features.blockersVisible);
- const showPins = getSettings(state).getIn(['account_timeline', 'shows', 'pinned']) && !withReplies;
-
- return {
- accountId,
- unavailable,
- accountUsername,
- accountApId,
- isBlocked,
- account,
- isAccount: !!state.getIn(['accounts', accountId]),
- statusIds: getStatusIds(state, { type: `account:${path}`, prefix: 'account_timeline' }),
- featuredStatusIds: showPins ? getStatusIds(state, { type: `account:${accountId}:pinned`, prefix: 'account_timeline' }) : ImmutableOrderedSet(),
- isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
- hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
- me,
- patronEnabled: soapboxConfig.getIn(['extensions', 'patron', 'enabled']),
- };
- };
-
- return mapStateToProps;
-};
-
-export default @connect(makeMapStateToProps)
-@withRouter
-class AccountTimeline extends ImmutablePureComponent {
-
- static propTypes = {
- params: PropTypes.object.isRequired,
- dispatch: PropTypes.func.isRequired,
- statusIds: ImmutablePropTypes.orderedSet,
- featuredStatusIds: ImmutablePropTypes.orderedSet,
- isLoading: PropTypes.bool,
- hasMore: PropTypes.bool,
- withReplies: PropTypes.bool,
- isAccount: PropTypes.bool,
- unavailable: PropTypes.bool,
- };
-
- componentDidMount() {
- const { params: { username }, accountId, accountApId, withReplies, patronEnabled, history } = this.props;
-
- this.props.dispatch(fetchAccountByUsername(username, history));
-
- if (accountId && accountId !== -1) {
- if (!withReplies) {
- this.props.dispatch(expandAccountFeaturedTimeline(accountId));
- }
-
- if (patronEnabled && accountApId) {
- this.props.dispatch(fetchPatronAccount(accountApId));
- }
-
- this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
- }
- }
-
- componentDidUpdate(prevProps) {
- const { params: { username }, accountId, withReplies, accountApId, patronEnabled, history } = this.props;
-
- if (username && (username !== prevProps.params.username)) {
- this.props.dispatch(fetchAccountByUsername(username, history));
- }
-
- if (accountId && (accountId !== -1) && (accountId !== prevProps.accountId) || withReplies !== prevProps.withReplies) {
- if (!withReplies) {
- this.props.dispatch(expandAccountFeaturedTimeline(accountId));
- }
-
- if (patronEnabled && accountApId) {
- this.props.dispatch(fetchPatronAccount(accountApId));
- }
-
- this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
- }
- }
-
- handleLoadMore = maxId => {
- if (this.props.accountId && this.props.accountId !== -1) {
- this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies }));
- }
- }
-
- render() {
- const { statusIds, featuredStatusIds, isLoading, hasMore, isBlocked, isAccount, accountId, unavailable, accountUsername } = this.props;
-
- if (!isAccount && accountId !== -1) {
- return (
-
- );
- }
-
- if (accountId === -1 || (!statusIds && isLoading)) {
- return (
-
- );
- }
-
- if (unavailable) {
- return (
-
-
-
- {isBlocked ? (
-
- ) : (
-
- )}
-
-
-
- );
- }
-
- return (
- }
- />
- );
- }
-
-}
diff --git a/app/soapbox/features/account_timeline/index.tsx b/app/soapbox/features/account_timeline/index.tsx
new file mode 100644
index 000000000..08ab71539
--- /dev/null
+++ b/app/soapbox/features/account_timeline/index.tsx
@@ -0,0 +1,111 @@
+import React, { useEffect, useState } from 'react';
+import { FormattedMessage } from 'react-intl';
+import { useHistory } from 'react-router-dom';
+
+import { fetchAccountByUsername } from 'soapbox/actions/accounts';
+import { fetchPatronAccount } from 'soapbox/actions/patron';
+import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'soapbox/actions/timelines';
+import MissingIndicator from 'soapbox/components/missing_indicator';
+import StatusList from 'soapbox/components/status_list';
+import { Card, CardBody, Spinner, Text } from 'soapbox/components/ui';
+import { useAppDispatch, useAppSelector, useFeatures, useSettings, useSoapboxConfig } from 'soapbox/hooks';
+import { makeGetStatusIds, findAccountByUsername } from 'soapbox/selectors';
+
+const getStatusIds = makeGetStatusIds();
+
+interface IAccountTimeline {
+ params: {
+ username: string,
+ },
+ withReplies?: boolean,
+}
+
+const AccountTimeline: React.FC = ({ params, withReplies = false }) => {
+ const history = useHistory();
+ const dispatch = useAppDispatch();
+ const features = useFeatures();
+ const settings = useSettings();
+ const soapboxConfig = useSoapboxConfig();
+
+ const account = useAppSelector(state => findAccountByUsername(state, params.username));
+ const [accountLoading, setAccountLoading] = useState(!account);
+
+ const path = withReplies ? `${account?.id}:with_replies` : account?.id;
+ const showPins = settings.getIn(['account_timeline', 'shows', 'pinned']) === true && !withReplies;
+ const statusIds = useAppSelector(state => getStatusIds(state, { type: `account:${path}`, prefix: 'account_timeline' }));
+ const featuredStatusIds = useAppSelector(state => getStatusIds(state, { type: `account:${account?.id}:pinned`, prefix: 'account_timeline' }));
+
+ const isBlocked = useAppSelector(state => state.relationships.getIn([account?.id, 'blocked_by']) === true);
+ const unavailable = isBlocked && !features.blockersVisible;
+ const patronEnabled = soapboxConfig.getIn(['extensions', 'patron', 'enabled']) === true;
+ const isLoading = useAppSelector(state => state.getIn(['timelines', `account:${path}`, 'isLoading']) === true);
+ const hasMore = useAppSelector(state => state.getIn(['timelines', `account:${path}`, 'hasMore']) === true);
+
+ const accountUsername = account?.username || params.username;
+
+ useEffect(() => {
+ dispatch(fetchAccountByUsername(params.username, history))
+ .then(() => setAccountLoading(false))
+ .catch(() => setAccountLoading(false));
+ }, [params.username]);
+
+ useEffect(() => {
+ if (account && !withReplies) {
+ dispatch(expandAccountFeaturedTimeline(account.id));
+ }
+ }, [account?.id, withReplies]);
+
+ useEffect(() => {
+ if (account && patronEnabled) {
+ dispatch(fetchPatronAccount(account.url));
+ }
+ }, [account?.url, patronEnabled]);
+
+ useEffect(() => {
+ if (account) {
+ dispatch(expandAccountTimeline(account.id, { withReplies }));
+ }
+ }, [account?.id]);
+
+ const handleLoadMore = (maxId: string) => {
+ if (account) {
+ dispatch(expandAccountTimeline(account.id, { maxId, withReplies }));
+ }
+ };
+
+ if (!account && accountLoading) {
+ return ;
+ } else if (!account) {
+ return ;
+ }
+
+ if (unavailable) {
+ return (
+
+
+
+ {isBlocked ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+ }
+
+ return (
+ }
+ />
+ );
+};
+
+export default AccountTimeline;
diff --git a/app/soapbox/pages/profile_page.tsx b/app/soapbox/pages/profile_page.tsx
index e188b897f..9c5a4a55e 100644
--- a/app/soapbox/pages/profile_page.tsx
+++ b/app/soapbox/pages/profile_page.tsx
@@ -2,6 +2,8 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Redirect, useHistory } from 'react-router-dom';
+import { Column, Layout, Tabs } from 'soapbox/components/ui';
+import Header from 'soapbox/features/account/components/header';
import LinkFooter from 'soapbox/features/ui/components/link_footer';
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import {
@@ -14,63 +16,35 @@ import {
PinnedAccountsPanel,
} from 'soapbox/features/ui/util/async-components';
import { useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
-import { findAccountByUsername } from 'soapbox/selectors';
+import { findAccountByUsername, makeGetAccount } from 'soapbox/selectors';
import { getAcct, isLocal } from 'soapbox/utils/accounts';
-import { Column, Layout, Tabs } from '../components/ui';
-import HeaderContainer from '../features/account_timeline/containers/header_container';
-import { makeGetAccount } from '../selectors';
-
-const getAccount = makeGetAccount();
-
interface IProfilePage {
params?: {
username?: string,
},
}
+const getAccount = makeGetAccount();
+
/** Page to display a user's profile. */
const ProfilePage: React.FC = ({ params, children }) => {
const history = useHistory();
const username = params?.username || '';
- const { accountId, account, realAccount } = useAppSelector(state => {
- const { accounts } = state;
- const accountFetchError = (((state.accounts.getIn([-1, 'username']) || '') as string).toLowerCase() === username.toLowerCase());
-
- let accountId: string | -1 | null = -1;
- let account = null;
- if (accountFetchError) {
- accountId = null;
- } else {
- account = findAccountByUsername(state, username);
- accountId = account ? account.id : -1;
- }
-
- let realAccount;
- if (!account) {
- const maybeAccount = accounts.get(username);
- if (maybeAccount) {
- realAccount = maybeAccount;
+ const account = useAppSelector(state => {
+ if (username) {
+ const account = findAccountByUsername(state, username);
+ if (account) {
+ return getAccount(state, account.id) || undefined;
}
}
-
- return {
- account: typeof accountId === 'string' ? getAccount(state, accountId) : account,
- accountId,
- realAccount,
- };
});
const me = useAppSelector(state => state.me);
const features = useFeatures();
const { displayFqn } = useSoapboxConfig();
- // Redirect from a user ID
- if (realAccount) {
- return ;
- }
-
// Fix case of username
if (account && account.acct !== username) {
return ;
@@ -107,25 +81,24 @@ const ProfilePage: React.FC = ({ params, children }) => {
let activeItem;
const pathname = history.location.pathname.replace(`@${username}/`, '');
- if (pathname.includes('with_replies')) {
+ if (pathname.endsWith('/with_replies')) {
activeItem = 'replies';
- } else if (pathname.includes('media')) {
+ } else if (pathname.endsWith('/media')) {
activeItem = 'media';
- } else if (pathname.includes('favorites')) {
+ } else if (pathname.endsWith('/favorites')) {
activeItem = 'likes';
} else {
activeItem = 'profile';
}
- const showTabs = !['following', 'followers', 'pins'].some(path => pathname.includes(path));
+ const showTabs = !['/following', '/followers', '/pins'].some(path => pathname.endsWith(path));
return (
<>
- {/* @ts-ignore */}
-
+
{Component => }