Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { css } from '@emotion/react';
import { isUndefined } from '@guardian/libs';
import {
article17,
headlineBold20,
headlineMedium20,
space,
} from '@guardian/source/foundations';
import { ExpandingWrapper } from '@guardian/source-development-kitchen/react-components';
import { palette } from '../palette';
import type { ReporterCalloutBlockElement } from '../types/content';

/**
* A callout to readers to share tips with reporters, typically focused on secure messaging (coverdrop), with references
* to securedrop, signal, protonmail etc.
*
*/

const expandingWrapperTheme = {
'--background': palette('--expandingWrapper--background'),
'--border': palette('--expandingWrapper--border'),
'--collapseBackground': palette('--expandingWrapper--collapseBackground'),
'--collapseBackgroundHover': palette(
'--expandingWrapper--collapseBackgroundHover',
),
'--collapseText': palette('--expandingWrapper--collapseText'),
'--collapseTextHover': palette('--expandingWrapper--collapseTextHover'),
'--text': palette('--expandingWrapper--text'),
'--horizontalRules': palette('--expandingWrapper--horizontalRules'),
'--expandBackground': palette('--expandingWrapper--expandBackground'),
'--expandBackgroundHover': palette(
'--expandingWrapper--expandBackgroundHover',
),
'--expandText': palette('--expandingWrapper--expandText'),
};

const promptStyles = css`
${headlineBold20};
`;

const subtitleTextHeaderStyles = css`
${headlineMedium20};
padding-bottom: ${space[3]}px;
`;

const reporterCalloutFieldStyles = css`
a {
color: ${palette('--article-link-text')};
text-decoration-color: ${palette('--article-link-border')};
text-underline-offset: 0.375em;
text-decoration-thickness: 1px;
}
a:hover,
a:active {
text-decoration-color: currentColor;
}
padding-bottom: ${space[4]}px;
${article17}
b {
font-weight: bold;
}
`;
const reporterCalloutWrapperStyles = css`
padding-bottom: ${space[4]}px;
margin-left: ${space[2]}px;
margin-right: ${space[2]}px;
`;

const dangerouslyRenderField = (field?: string, title?: string) => {
return field ? (
<div css={reporterCalloutFieldStyles}>
{!!title && <h4 css={subtitleTextHeaderStyles}>{title}</h4>}
<div
dangerouslySetInnerHTML={{
__html: field,
}}
/>
</div>
) : (
''
);
};

export const ReporterCalloutBlockComponent = ({
callout,
}: {
callout: ReporterCalloutBlockElement;
}) => {
const {
title,
subtitle,
intro,
mainText,
mainTextHeading,
emailContact,
messagingContact,
securedropContact,
endNote,
activeUntil,
} = callout;
const isExpired = isUndefined(activeUntil)
? false
: Math.floor(new Date().getTime() / 1000) > activeUntil;

return isExpired ? (
<></>
) : (
<div data-gu-name="reporter-callout">
<ExpandingWrapper
name={`${title} reporter callout`}
theme={expandingWrapperTheme}
collapsedHeight={'160px'}
>
<div css={reporterCalloutWrapperStyles}>
<div
css={promptStyles}
style={{
// reuse atom styling for now unless we get special callout designs
color: palette('--expandable-atom-text-hover'),
}}
>
{title}
</div>

<h4 css={subtitleTextHeaderStyles}>{subtitle}</h4>

{dangerouslyRenderField(intro)}
{dangerouslyRenderField(mainText, mainTextHeading)}
{dangerouslyRenderField(emailContact, 'Email')}
{dangerouslyRenderField(messagingContact, 'Messaging apps')}
{dangerouslyRenderField(securedropContact, 'SecureDrop')}
{dangerouslyRenderField(endNote)}
</div>
</ExpandingWrapper>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { splitTheme } from '../../.storybook/decorators/splitThemeDecorator';
import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat';
import type { ReporterCalloutBlockElement } from '../types/content';
import { ReporterCalloutBlockComponent } from './ReporterCalloutBlockComponent.importable';

const meta: Meta<typeof ReporterCalloutBlockComponent> = {
title: 'Components/Reporter Callout Block',
component: ReporterCalloutBlockComponent,
};

export default meta;

type Story = StoryObj<typeof ReporterCalloutBlockComponent>;

const defaultFormat = {
display: ArticleDisplay.Standard,
design: ArticleDesign.Standard,
theme: Pillar.News,
};

const defaultCallout: ReporterCalloutBlockElement = {
_type: 'model.dotcomrendering.pageElements.ReporterCalloutBlockElement',
elementId: 'abc123',
id: 'def456',
displayOnSensitive: false,
title: 'Get in touch',
subtitle: 'Contact us about this story',
intro: '<p>If you have something to share about this story, please contact us using one of the following methods</p>',
mainTextHeading: 'Secure Messaging in the Guardian app',
mainText:
'<p>The Guardian app has a tool to send tips about stories. Messages are end to end encrypted and concealed within the routine activity that every Guardian mobile app performs. This prevents an observer from knowing that you are communicating with us at all, let alone what is being said.</p><p></p><p>If you don\'t already have the Guardian app, download it (<a href="https://apps.apple.com/app/the-guardian-live-world-news/id409128287">iOS</a>/<a href="https://play.google.com/store/apps/details?id=com.guardian">Android</a>) and go to the menu. Select \'Secure Messaging\'.</p><p> </p>',
emailContact:
'<p>If you don\'t need strong security then see <a href="https://manage.theguardian.com/help-centre/article/contact-a-journalist-or-editorial-desk">this guide</a>&nbsp;for how to contact the relevant desk</p>',
messagingContact:
'<p>Alternatively put a message in a bottle and lob it at +447123456789</p>',
securedropContact:
'If you can safely use the tor network without being observed or monitored you can send messages and documents to the Guardian via our <a href="https://www.theguardian.com/securedrop">SecureDrop platform.</a>',
endNote:
'<p>Finally, our guide at <a href="https://www.theguardian.com/tips">theguardian.com/tips</a> lists several ways to contact us securely, and discusses the pros and cons of each.</p>',
};

export const Default: Story = {
args: {
callout: defaultCallout,
},
decorators: [splitTheme([defaultFormat])],
};

export const DefaultSport: Story = {
args: {
callout: defaultCallout,
},
decorators: [splitTheme([{ ...defaultFormat, theme: Pillar.Sport }])],
};

export const MinimalContacts: Story = {
args: {
callout: {
...defaultCallout,
emailContact: undefined,
messagingContact: undefined,
securedropContact: undefined,
},
},
decorators: [splitTheme([defaultFormat])],
};

export const WithoutEndNote: Story = {
args: {
callout: {
...defaultCallout,
endNote: undefined,
},
},
decorators: [splitTheme([defaultFormat])],
};

export const ExpiredCallout: Story = {
args: {
callout: {
...defaultCallout,
// Set activeUntil to a timestamp in the past (January 1, 2020)
activeUntil: 1577836800,
},
},
decorators: [splitTheme([defaultFormat])],
};
68 changes: 68 additions & 0 deletions dotcom-rendering/src/frontend/schemas/feArticle.json
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,9 @@
{
"$ref": "#/definitions/CalloutBlockElementV2"
},
{
"$ref": "#/definitions/ReporterCalloutBlockElement"
},
{
"$ref": "#/definitions/CartoonBlockElement"
},
Expand Down Expand Up @@ -1547,6 +1550,71 @@
"value"
]
},
"ReporterCalloutBlockElement": {
"type": "object",
"properties": {
"_type": {
"type": "string",
"const": "model.dotcomrendering.pageElements.ReporterCalloutBlockElement"
},
"elementId": {
"type": "string"
},
"id": {
"type": "string"
},
"activeFrom": {
"type": "number"
},
"activeUntil": {
"type": "number"
},
"displayOnSensitive": {
"type": "boolean"
},
"role": {
"$ref": "#/definitions/RoleType"
},
"title": {
"type": "string"
},
"subtitle": {
"type": "string"
},
"intro": {
"type": "string"
},
"mainTextHeading": {
"type": "string"
},
"mainText": {
"type": "string"
},
"emailContact": {
"type": "string"
},
"messagingContact": {
"type": "string"
},
"securedropContact": {
"type": "string"
},
"endNote": {
"type": "string"
}
},
"required": [
"_type",
"displayOnSensitive",
"elementId",
"id",
"intro",
"mainText",
"mainTextHeading",
"subtitle",
"title"
]
},
"CartoonBlockElement": {
"type": "object",
"properties": {
Expand Down
10 changes: 10 additions & 0 deletions dotcom-rendering/src/lib/renderElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { ProfileAtomWrapper } from '../components/ProfileAtomWrapper.importable'
import { PullQuoteBlockComponent } from '../components/PullQuoteBlockComponent';
import { QandaAtom } from '../components/QandaAtom.importable';
import { QAndAExplainers } from '../components/QAndAExplainers';
import { ReporterCalloutBlockComponent } from '../components/ReporterCalloutBlockComponent.importable';
import { RichLinkComponent } from '../components/RichLinkComponent.importable';
import { SelfHostedVideoInArticle } from '../components/SelfHostedVideoInArticle';
import { SoundcloudBlockComponent } from '../components/SoundcloudBlockComponent';
Expand Down Expand Up @@ -223,6 +224,15 @@ export const renderElement = ({
);
}
return null;
case 'model.dotcomrendering.pageElements.ReporterCalloutBlockElement':
if (switches.callouts) {
return (
<Island priority="feature" defer={{ until: 'visible' }}>
<ReporterCalloutBlockComponent callout={element} />
</Island>
);
}
return null;

case 'model.dotcomrendering.pageElements.CaptionBlockElement':
return (
Expand Down
Loading
Loading