src/amo/components/AddonMoreInfo/index.js (442 lines of code) (raw):
/* @flow */
import * as React from 'react';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
import AddonAdminLinks from 'amo/components/AddonAdminLinks';
import AddonAuthorLinks from 'amo/components/AddonAuthorLinks';
import AddonReportAbuseLink from 'amo/components/AddonReportAbuseLink';
import Card from 'amo/components/Card';
import DefinitionList, { Definition } from 'amo/components/DefinitionList';
import Link from 'amo/components/Link';
import LoadingText from 'amo/components/LoadingText';
import {
ADDON_TYPE_EXTENSION,
ADDON_TYPE_LANG,
ADDON_TYPE_STATIC_THEME,
STATS_VIEW,
} from 'amo/constants';
import { withErrorHandler } from 'amo/errorHandler';
import translate from 'amo/i18n/translate';
import { fetchCategories } from 'amo/reducers/categories';
import { hasPermission } from 'amo/reducers/users';
import { getVersionById, getVersionInfo } from 'amo/reducers/versions';
import { isAddonAuthor } from 'amo/utils';
import { getCategoryResultsPathname } from 'amo/utils/categories';
import { getTagResultsPathname } from 'amo/utils/tags';
import {
addQueryParams,
getQueryParametersForAttribution,
} from 'amo/utils/url';
import type { UserId } from 'amo/reducers/users';
import type { AddonVersionType, VersionInfoType } from 'amo/reducers/versions';
import type { AppState } from 'amo/store';
import type { AddonType } from 'amo/types/addons';
import type { ErrorHandlerType } from 'amo/types/errorHandler';
import type { I18nType } from 'amo/types/i18n';
import type { DispatchFunc } from 'amo/types/redux';
import type { ReactRouterLocationType } from 'amo/types/router';
type Props = {|
addon: AddonType | null,
i18n: I18nType,
|};
type PropsFromState = {|
categoriesLoading: boolean,
currentVersion: AddonVersionType | null,
hasStatsPermission: boolean,
relatedCategories: Array<Object> | null,
userId: UserId | null,
versionInfo: VersionInfoType | null,
|};
type InternalProps = {|
...Props,
...PropsFromState,
dispatch: DispatchFunc,
errorHandler: ErrorHandlerType,
location: ReactRouterLocationType,
|};
export class AddonMoreInfoBase extends React.Component<InternalProps> {
constructor(props: InternalProps) {
super(props);
const { categoriesLoading, dispatch, errorHandler, relatedCategories } =
props;
if (!categoriesLoading && !relatedCategories) {
dispatch(fetchCategories({ errorHandlerId: errorHandler.id }));
}
}
listContent(): React.Node {
const {
addon,
currentVersion,
hasStatsPermission,
i18n,
location,
relatedCategories,
userId,
versionInfo,
} = this.props;
if (!addon) {
return this.renderDefinitions({
versionLastUpdated: <LoadingText minWidth={20} />,
versionLicense: <LoadingText minWidth={20} />,
});
}
let homepage: null | React.Element<'li'> | string =
addon.homepage && addon.homepage.outgoing;
if (homepage) {
homepage = (
<li>
<a
className="AddonMoreInfo-homepage-link"
href={homepage}
title={addon.homepage && addon.homepage.url}
rel="nofollow"
>
{i18n.gettext('Homepage')}
</a>
</li>
);
}
let supportUrl: null | React.Element<'li'> | string =
addon.support_url && addon.support_url.outgoing;
if (supportUrl) {
supportUrl = (
<li>
<a
className="AddonMoreInfo-support-link"
href={supportUrl}
title={addon.support_url && addon.support_url.url}
rel="nofollow"
>
{i18n.gettext('Support site')}
</a>
</li>
);
}
let supportEmail: React.Element<'li'> | null;
if (addon.support_email && /.+@.+/.test(addon.support_email)) {
supportEmail = (
<li>
<a
className="AddonMoreInfo-support-email"
href={`mailto:${addon.support_email}`}
>
{i18n.gettext('Support Email')}
</a>
</li>
);
} else {
supportEmail = null;
}
let statsLink = null;
if (isAddonAuthor({ addon, userId }) || hasStatsPermission) {
statsLink = (
<Link
className="AddonMoreInfo-stats-link"
href={addQueryParams(
`/addon/${addon.slug}/statistics/`,
getQueryParametersForAttribution(location),
)}
>
{i18n.gettext('Visit stats dashboard')}
</Link>
);
}
const lastUpdated = versionInfo && versionInfo.created;
const license = currentVersion && currentVersion.license;
let versionLicenseLink = null;
if (license) {
const linkProps = license.isCustom
? {
to: addQueryParams(
`/addon/${addon.slug}/license/`,
getQueryParametersForAttribution(location),
),
rel: 'nofollow',
}
: { href: license.url, prependClientApp: false, prependLang: false };
const licenseName = license.name || i18n.gettext('Custom License');
versionLicenseLink = license.url ? (
<Link className="AddonMoreInfo-license-link" {...linkProps}>
{licenseName}
</Link>
) : (
<span className="AddonMoreInfo-license-name">{licenseName}</span>
);
}
let categories = null;
if (
[ADDON_TYPE_EXTENSION, ADDON_TYPE_STATIC_THEME].includes(addon.type) &&
relatedCategories &&
relatedCategories.length > 0
) {
categories = relatedCategories.map((category) => {
return (
<li key={category.slug}>
<Link
className="AddonMoreInfo-related-category-link"
to={getCategoryResultsPathname({
addonType: category.type,
slug: category.slug,
})}
>
{i18n.gettext(category.name)}
</Link>
</li>
);
});
}
return this.renderDefinitions({
homepage,
supportUrl,
supportEmail,
statsLink,
version: currentVersion ? currentVersion.version : null,
filesize: versionInfo && versionInfo.filesize,
versionLastUpdated: lastUpdated
? i18n.sprintf(
// L10n: This will output, in English: "2 months ago (Dec 12 2016)"
i18n.gettext('%(timeFromNow)s (%(date)s)'),
{
timeFromNow: i18n.moment(lastUpdated).fromNow(),
date: i18n.moment(lastUpdated).format('ll'),
},
)
: null,
versionLicenseLink,
privacyPolicyLink: addon.has_privacy_policy ? (
<Link
className="AddonMoreInfo-privacy-policy-link"
to={addQueryParams(
`/addon/${addon.slug}/privacy/`,
getQueryParametersForAttribution(location),
)}
rel="nofollow"
>
{i18n.gettext('Read the privacy policy for this add-on')}
</Link>
) : null,
eulaLink: addon.has_eula ? (
<Link
className="AddonMoreInfo-eula-link"
to={addQueryParams(
`/addon/${addon.slug}/eula/`,
getQueryParametersForAttribution(location),
)}
rel="nofollow"
>
{i18n.gettext('Read the license agreement for this add-on')}
</Link>
) : null,
relatedCategories: categories,
versionHistoryLink: (
<li>
<Link
className="AddonMoreInfo-version-history-link"
to={addQueryParams(
`/addon/${addon.slug}/versions/`,
getQueryParametersForAttribution(location),
)}
>
{i18n.gettext('See all versions')}
</Link>
</li>
),
tagsLinks:
addon.tags.length > 0
? addon.tags.map((tag) => {
return (
<li key={tag}>
<Link
className="AddonMoreInfo-tag-link"
to={addQueryParams(
getTagResultsPathname({ tag }),
getQueryParametersForAttribution(location),
)}
>
{tag}
</Link>
</li>
);
})
: null,
});
}
renderDefinitions({
eulaLink = null,
filesize = null,
homepage = null,
privacyPolicyLink = null,
relatedCategories = null,
statsLink = null,
supportEmail = null,
supportUrl = null,
tagsLinks = null,
version = null,
versionHistoryLink = null,
versionLastUpdated,
versionLicenseLink = null,
}: Object): React.Node {
const { addon, i18n } = this.props;
return (
<>
<DefinitionList className="AddonMoreInfo-dl">
{(homepage || supportUrl || supportEmail) && (
<Definition
className="AddonMoreInfo-links"
term={i18n.gettext('Add-on Links')}
>
<ul className="AddonMoreInfo-links-contents-list">
{homepage}
{supportUrl}
{supportEmail}
</ul>
</Definition>
)}
{version && (
<Definition
className="AddonMoreInfo-version"
term={i18n.gettext('Version')}
>
{version}
</Definition>
)}
{filesize && (
<Definition
className="AddonMoreInfo-filesize"
term={i18n.gettext('Size')}
>
{filesize}
</Definition>
)}
{versionLastUpdated && (
<Definition
className="AddonMoreInfo-last-updated"
term={i18n.gettext('Last updated')}
>
{versionLastUpdated}
</Definition>
)}
{relatedCategories && (
<Definition
className="AddonMoreInfo-related-categories"
term={i18n.gettext('Related Categories')}
>
<ul className="AddonMoreInfo-related-categories-list">
{relatedCategories}
</ul>
</Definition>
)}
{versionLicenseLink && (
<Definition
className="AddonMoreInfo-license"
term={i18n.gettext('License')}
>
{versionLicenseLink}
</Definition>
)}
{privacyPolicyLink && (
<Definition
className="AddonMoreInfo-privacy-policy"
term={i18n.gettext('Privacy Policy')}
>
{privacyPolicyLink}
</Definition>
)}
{eulaLink && (
<Definition
className="AddonMoreInfo-eula"
term={i18n.gettext('End-User License Agreement')}
>
{eulaLink}
</Definition>
)}
{versionHistoryLink && (
<Definition
className="AddonMoreInfo-version-history"
term={i18n.gettext('Version History')}
>
<ul className="AddonMoreInfo-links-contents-list">
{versionHistoryLink}
</ul>
</Definition>
)}
{statsLink && (
<Definition
className="AddonMoreInfo-stats"
term={i18n.gettext('Usage Statistics')}
>
{statsLink}
</Definition>
)}
{tagsLinks && (
<Definition
className="AddonMoreInfo-tag-links"
term={i18n.gettext('Tags')}
>
<ul className="AddonMoreInfo-tag-links-list">{tagsLinks}</ul>
</Definition>
)}
</DefinitionList>
<AddonAdminLinks addon={addon} />
<AddonAuthorLinks addon={addon} />
{addon && addon.type !== ADDON_TYPE_LANG && (
<AddonReportAbuseLink addon={addon} />
)}
</>
);
}
render(): React.Node {
const { errorHandler, i18n } = this.props;
return (
<Card className="AddonMoreInfo" header={i18n.gettext('More information')}>
{errorHandler.renderErrorIfPresent()}
{this.listContent()}
</Card>
);
}
}
const mapStateToProps = (state: AppState, ownProps: Props): PropsFromState => {
const { addon, i18n } = ownProps;
const { categories } = state.categories;
let currentVersion = null;
let relatedCategories = null;
let versionInfo = null;
if (addon && addon.currentVersionId) {
currentVersion = getVersionById({
id: addon.currentVersionId,
state: state.versions,
});
}
if (currentVersion) {
versionInfo = getVersionInfo({
i18n,
state: state.versions,
versionId: currentVersion.id,
});
}
if (addon && addon.categories && addon.type && categories) {
const appCategories = categories[addon.type];
const addonCategories = addon.categories || [];
relatedCategories = addonCategories.reduce((result, slug) => {
if (typeof appCategories[slug] !== 'undefined') {
result.push(appCategories[slug]);
}
return result;
}, []);
}
return {
currentVersion,
relatedCategories,
versionInfo,
categoriesLoading: state.categories.loading,
hasStatsPermission: hasPermission(state, STATS_VIEW),
userId: state.users.currentUserID,
};
};
const AddonMoreInfo: React.ComponentType<Props> = compose(
withRouter,
translate(),
connect(mapStateToProps),
withErrorHandler({ name: 'AddonMoreInfo' }),
)(AddonMoreInfoBase);
export default AddonMoreInfo;