ProfileInfoPanel: convert to TSX
This commit is contained in:
parent
580960695c
commit
0127d282e2
2 changed files with 230 additions and 271 deletions
|
@ -1,271 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
import { List as ImmutableList } 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 { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { initAccountNoteModal } from 'soapbox/actions/account_notes';
|
|
||||||
import Badge from 'soapbox/components/badge';
|
|
||||||
import { Icon, HStack, Stack, Text } from 'soapbox/components/ui';
|
|
||||||
import VerificationBadge from 'soapbox/components/verification_badge';
|
|
||||||
import { isLocal } from 'soapbox/utils/accounts';
|
|
||||||
import { displayFqn } from 'soapbox/utils/state';
|
|
||||||
|
|
||||||
import ProfileStats from './profile_stats';
|
|
||||||
|
|
||||||
// Basically ensure the URL isn't `javascript:alert('hi')` or something like that
|
|
||||||
const isSafeUrl = text => {
|
|
||||||
try {
|
|
||||||
const url = new URL(text);
|
|
||||||
return ['http:', 'https:'].includes(url.protocol);
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
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.' },
|
|
||||||
deactivated: { id: 'account.deactivated', defaultMessage: 'Deactivated' },
|
|
||||||
bot: { id: 'account.badges.bot', defaultMessage: 'Bot' },
|
|
||||||
});
|
|
||||||
|
|
||||||
class ProfileInfoPanel extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
account: ImmutablePropTypes.record,
|
|
||||||
identity_proofs: ImmutablePropTypes.list,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
username: PropTypes.string,
|
|
||||||
displayFqn: PropTypes.bool,
|
|
||||||
onShowNote: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
getStaffBadge = () => {
|
|
||||||
const { account } = this.props;
|
|
||||||
|
|
||||||
if (account?.admin) {
|
|
||||||
return <Badge slug='admin' title='Admin' key='staff' />;
|
|
||||||
} else if (account?.moderator) {
|
|
||||||
return <Badge slug='moderator' title='Moderator' key='staff' />;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getBadges = () => {
|
|
||||||
const { account } = this.props;
|
|
||||||
const staffBadge = this.getStaffBadge();
|
|
||||||
const isPatron = account.getIn(['patron', 'is_patron']);
|
|
||||||
|
|
||||||
const badges = [];
|
|
||||||
|
|
||||||
if (staffBadge) {
|
|
||||||
badges.push(staffBadge);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPatron) {
|
|
||||||
badges.push(<Badge slug='patron' title='Patron' key='patron' />);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.donor) {
|
|
||||||
badges.push(<Badge slug='donor' title='Donor' key='donor' />);
|
|
||||||
}
|
|
||||||
|
|
||||||
return badges;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderBirthday = () => {
|
|
||||||
const { account, intl } = this.props;
|
|
||||||
|
|
||||||
const birthday = account.get('birthday');
|
|
||||||
if (!birthday) return null;
|
|
||||||
|
|
||||||
const formattedBirthday = intl.formatDate(birthday, { timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' });
|
|
||||||
|
|
||||||
const date = new Date(birthday);
|
|
||||||
const today = new Date();
|
|
||||||
|
|
||||||
const hasBirthday = date.getDate() === today.getDate() && date.getMonth() === today.getMonth();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HStack alignItems='center' space={0.5}>
|
|
||||||
<Icon
|
|
||||||
src={require('@tabler/icons/icons/ballon.svg')}
|
|
||||||
className='w-4 h-4 text-gray-800 dark:text-gray-200'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text size='sm'>
|
|
||||||
{hasBirthday ? (
|
|
||||||
<FormattedMessage id='account.birthday_today' defaultMessage='Birthday is today!' />
|
|
||||||
) : (
|
|
||||||
<FormattedMessage id='account.birthday' defaultMessage='Born {date}' values={{ date: formattedBirthday }} />
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleShowNote = e => {
|
|
||||||
const { account, onShowNote } = this.props;
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
onShowNote(account);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { account, displayFqn, intl, username } = this.props;
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
return (
|
|
||||||
<div className='mt-6 min-w-0 flex-1 sm:px-2'>
|
|
||||||
<Stack space={2}>
|
|
||||||
<Stack>
|
|
||||||
<HStack space={1} alignItems='center'>
|
|
||||||
<Text size='sm' theme='muted'>
|
|
||||||
@{username}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = { __html: account.get('note_emojified') };
|
|
||||||
const deactivated = !account.getIn(['pleroma', 'is_active'], true);
|
|
||||||
const displayNameHtml = deactivated ? { __html: intl.formatMessage(messages.deactivated) } : { __html: account.get('display_name_html') };
|
|
||||||
const memberSinceDate = intl.formatDate(account.get('created_at'), { month: 'long', year: 'numeric' });
|
|
||||||
const verified = account.get('verified');
|
|
||||||
const badges = this.getBadges();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='mt-6 min-w-0 flex-1 sm:px-2'>
|
|
||||||
<Stack space={2}>
|
|
||||||
{/* Not sure if this is actual used. */}
|
|
||||||
{/* <div className='profile-info-panel-content__deactivated'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='account.deactivated_description' defaultMessage='This account has been deactivated.'
|
|
||||||
/>
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
<Stack>
|
|
||||||
<HStack space={1} alignItems='center'>
|
|
||||||
<Text size='lg' weight='bold' dangerouslySetInnerHTML={displayNameHtml} />
|
|
||||||
|
|
||||||
{verified && <VerificationBadge />}
|
|
||||||
|
|
||||||
{account.bot && <Badge slug='bot' title={intl.formatMessage(messages.bot)} />}
|
|
||||||
|
|
||||||
{badges.length > 0 && (
|
|
||||||
<HStack space={1} alignItems='center'>
|
|
||||||
{badges}
|
|
||||||
</HStack>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
<HStack alignItems='center' space={0.5}>
|
|
||||||
<Text size='sm' theme='muted'>
|
|
||||||
@{displayFqn ? account.fqn : account.acct}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{account.get('locked') && (
|
|
||||||
<Icon
|
|
||||||
src={require('@tabler/icons/icons/lock.svg')}
|
|
||||||
title={intl.formatMessage(messages.account_locked)}
|
|
||||||
className='w-4 h-4 text-gray-600'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<ProfileStats account={account} />
|
|
||||||
|
|
||||||
{
|
|
||||||
(account.get('note').length > 0 && account.get('note') !== '<p></p>') &&
|
|
||||||
<Text size='sm' dangerouslySetInnerHTML={content} />
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className='flex flex-col md:flex-row items-start md:flex-wrap md:items-center gap-2'>
|
|
||||||
{isLocal(account) ? (
|
|
||||||
<HStack alignItems='center' space={0.5}>
|
|
||||||
<Icon
|
|
||||||
src={require('@tabler/icons/icons/calendar.svg')}
|
|
||||||
className='w-4 h-4 text-gray-800 dark:text-gray-200'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text size='sm'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='account.member_since' defaultMessage='Joined {date}' values={{
|
|
||||||
date: memberSinceDate,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{account.get('location') ? (
|
|
||||||
<HStack alignItems='center' space={0.5}>
|
|
||||||
<Icon
|
|
||||||
src={require('@tabler/icons/icons/map-pin.svg')}
|
|
||||||
className='w-4 h-4 text-gray-800 dark:text-gray-200'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text size='sm'>
|
|
||||||
{account.get('location')}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{account.get('website') ? (
|
|
||||||
<HStack alignItems='center' space={0.5}>
|
|
||||||
<Icon
|
|
||||||
src={require('@tabler/icons/icons/link.svg')}
|
|
||||||
className='w-4 h-4 text-gray-800 dark:text-gray-200'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='max-w-[300px]'>
|
|
||||||
<Text size='sm' truncate>
|
|
||||||
{isSafeUrl(account.get('website')) ? (
|
|
||||||
<a className='text-primary-600 dark:text-primary-400 hover:underline' href={account.get('website')} target='_blank'>{account.get('website')}</a>
|
|
||||||
) : (
|
|
||||||
account.get('website')
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</HStack>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{this.renderBirthday()}
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { account }) => {
|
|
||||||
const identity_proofs = account ? state.getIn(['identity_proofs', account.get('id')], ImmutableList()) : ImmutableList();
|
|
||||||
return {
|
|
||||||
identity_proofs,
|
|
||||||
domain: state.getIn(['meta', 'domain']),
|
|
||||||
displayFqn: displayFqn(state),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
onShowNote(account) {
|
|
||||||
dispatch(initAccountNoteModal(account));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default injectIntl(
|
|
||||||
connect(mapStateToProps, mapDispatchToProps, null, {
|
|
||||||
forwardRef: true,
|
|
||||||
},
|
|
||||||
)(ProfileInfoPanel));
|
|
230
app/soapbox/features/ui/components/profile_info_panel.tsx
Normal file
230
app/soapbox/features/ui/components/profile_info_panel.tsx
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import Badge from 'soapbox/components/badge';
|
||||||
|
import { Icon, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||||
|
import VerificationBadge from 'soapbox/components/verification_badge';
|
||||||
|
import { useSoapboxConfig } from 'soapbox/hooks';
|
||||||
|
import { isLocal } from 'soapbox/utils/accounts';
|
||||||
|
|
||||||
|
import ProfileStats from './profile_stats';
|
||||||
|
|
||||||
|
import type { Account } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
/** Basically ensure the URL isn't `javascript:alert('hi')` or something like that */
|
||||||
|
const isSafeUrl = (text: string): boolean => {
|
||||||
|
try {
|
||||||
|
const url = new URL(text);
|
||||||
|
return ['http:', 'https:'].includes(url.protocol);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
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.' },
|
||||||
|
deactivated: { id: 'account.deactivated', defaultMessage: 'Deactivated' },
|
||||||
|
bot: { id: 'account.badges.bot', defaultMessage: 'Bot' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IProfileInfoPanel {
|
||||||
|
account: Account,
|
||||||
|
/** Username from URL params, in case the account isn't found. */
|
||||||
|
username: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** User profile metadata, such as location, birthday, etc. */
|
||||||
|
const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { displayFqn } = useSoapboxConfig();
|
||||||
|
|
||||||
|
const getStaffBadge = (): React.ReactNode => {
|
||||||
|
if (account?.admin) {
|
||||||
|
return <Badge slug='admin' title='Admin' key='staff' />;
|
||||||
|
} else if (account?.moderator) {
|
||||||
|
return <Badge slug='moderator' title='Moderator' key='staff' />;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBadges = (): React.ReactNode[] => {
|
||||||
|
const staffBadge = getStaffBadge();
|
||||||
|
const isPatron = account.getIn(['patron', 'is_patron']) === true;
|
||||||
|
|
||||||
|
const badges = [];
|
||||||
|
|
||||||
|
if (staffBadge) {
|
||||||
|
badges.push(staffBadge);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPatron) {
|
||||||
|
badges.push(<Badge slug='patron' title='Patron' key='patron' />);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account.donor) {
|
||||||
|
badges.push(<Badge slug='donor' title='Donor' key='donor' />);
|
||||||
|
}
|
||||||
|
|
||||||
|
return badges;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBirthday = (): React.ReactNode => {
|
||||||
|
const birthday = account.birthday;
|
||||||
|
if (!birthday) return null;
|
||||||
|
|
||||||
|
const formattedBirthday = intl.formatDate(birthday, { timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' });
|
||||||
|
|
||||||
|
const date = new Date(birthday);
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
const hasBirthday = date.getDate() === today.getDate() && date.getMonth() === today.getMonth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack alignItems='center' space={0.5}>
|
||||||
|
<Icon
|
||||||
|
src={require('@tabler/icons/icons/ballon.svg')}
|
||||||
|
className='w-4 h-4 text-gray-800 dark:text-gray-200'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text size='sm'>
|
||||||
|
{hasBirthday ? (
|
||||||
|
<FormattedMessage id='account.birthday_today' defaultMessage='Birthday is today!' />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage id='account.birthday' defaultMessage='Born {date}' values={{ date: formattedBirthday }} />
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return (
|
||||||
|
<div className='mt-6 min-w-0 flex-1 sm:px-2'>
|
||||||
|
<Stack space={2}>
|
||||||
|
<Stack>
|
||||||
|
<HStack space={1} alignItems='center'>
|
||||||
|
<Text size='sm' theme='muted'>
|
||||||
|
@{username}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = { __html: account.note_emojified };
|
||||||
|
const deactivated = !account.pleroma.get('is_active', true) === true;
|
||||||
|
const displayNameHtml = deactivated ? { __html: intl.formatMessage(messages.deactivated) } : { __html: account.display_name_html };
|
||||||
|
const memberSinceDate = intl.formatDate(account.created_at, { month: 'long', year: 'numeric' });
|
||||||
|
const verified = account.verified;
|
||||||
|
const badges = getBadges();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-6 min-w-0 flex-1 sm:px-2'>
|
||||||
|
<Stack space={2}>
|
||||||
|
{/* Not sure if this is actual used. */}
|
||||||
|
{/* <div className='profile-info-panel-content__deactivated'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.deactivated_description' defaultMessage='This account has been deactivated.'
|
||||||
|
/>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
<Stack>
|
||||||
|
<HStack space={1} alignItems='center'>
|
||||||
|
<Text size='lg' weight='bold' dangerouslySetInnerHTML={displayNameHtml} />
|
||||||
|
|
||||||
|
{verified && <VerificationBadge />}
|
||||||
|
|
||||||
|
{account.bot && <Badge slug='bot' title={intl.formatMessage(messages.bot)} />}
|
||||||
|
|
||||||
|
{badges.length > 0 && (
|
||||||
|
<HStack space={1} alignItems='center'>
|
||||||
|
{badges}
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack alignItems='center' space={0.5}>
|
||||||
|
<Text size='sm' theme='muted'>
|
||||||
|
@{displayFqn ? account.fqn : account.acct}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{account.locked && (
|
||||||
|
<Icon
|
||||||
|
src={require('@tabler/icons/icons/lock.svg')}
|
||||||
|
alt={intl.formatMessage(messages.account_locked)}
|
||||||
|
className='w-4 h-4 text-gray-600'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<ProfileStats account={account} />
|
||||||
|
|
||||||
|
{account.note.length > 0 && account.note !== '<p></p>' && (
|
||||||
|
<Text size='sm' dangerouslySetInnerHTML={content} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='flex flex-col md:flex-row items-start md:flex-wrap md:items-center gap-2'>
|
||||||
|
{isLocal(account as any) ? (
|
||||||
|
<HStack alignItems='center' space={0.5}>
|
||||||
|
<Icon
|
||||||
|
src={require('@tabler/icons/icons/calendar.svg')}
|
||||||
|
className='w-4 h-4 text-gray-800 dark:text-gray-200'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text size='sm'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.member_since' defaultMessage='Joined {date}' values={{
|
||||||
|
date: memberSinceDate,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{account.location ? (
|
||||||
|
<HStack alignItems='center' space={0.5}>
|
||||||
|
<Icon
|
||||||
|
src={require('@tabler/icons/icons/map-pin.svg')}
|
||||||
|
className='w-4 h-4 text-gray-800 dark:text-gray-200'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text size='sm'>
|
||||||
|
{account.location}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{account.website ? (
|
||||||
|
<HStack alignItems='center' space={0.5}>
|
||||||
|
<Icon
|
||||||
|
src={require('@tabler/icons/icons/link.svg')}
|
||||||
|
className='w-4 h-4 text-gray-800 dark:text-gray-200'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='max-w-[300px]'>
|
||||||
|
<Text size='sm' truncate>
|
||||||
|
{isSafeUrl(account.website) ? (
|
||||||
|
<a className='text-primary-600 dark:text-primary-400 hover:underline' href={account.website} target='_blank'>{account.website}</a>
|
||||||
|
) : (
|
||||||
|
account.website
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</HStack>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{renderBirthday()}
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfileInfoPanel;
|
Loading…
Reference in a new issue