@financial-times/n-myft-ui 23.0.1 → 24.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.circleci/config.yml +13 -16
- package/.circleci/shared-helpers/helper-npm-install-peer-deps +6 -5
- package/.github/settings.yml +1 -1
- package/.scss-lint.yml +3 -3
- package/Makefile +1 -0
- package/README.md +11 -8
- package/build-state/npm-shrinkwrap.json +11383 -10733
- package/components/collections/collections.html +3 -11
- package/components/concept-list/concept-list.html +1 -4
- package/components/csrf-token/__tests__/input.test.js +23 -0
- package/components/csrf-token/input.jsx +26 -0
- package/components/follow-button/__tests__/follow-button.test.js +40 -0
- package/components/follow-button/follow-button.jsx +174 -0
- package/components/instant-alert/instant-alert.html +1 -1
- package/components/pin-button/pin-button.html +1 -1
- package/components/save-for-later/save-for-later.html +6 -9
- package/components/unread-articles-indicator/README.md +2 -62
- package/components/unread-articles-indicator/date-fns.js +5 -12
- package/components/unread-articles-indicator/index.js +1 -62
- package/components/unread-articles-indicator/storage.js +0 -44
- package/demos/app.js +30 -3
- package/demos/templates/demo.html +2 -4
- package/demos/templates/demo.jsx +33 -0
- package/demos/templates/digest-on-follow.html +1 -1
- package/jest.config.js +8 -0
- package/mixins/lozenge/_themes.scss +8 -4
- package/mixins/lozenge/main.scss +50 -4
- package/myft/main.scss +0 -1
- package/myft/ui/index.js +0 -2
- package/package.json +20 -6
- package/test/unread-articles-indicator/index.spec.js +13 -65
- package/test/unread-articles-indicator/storage.spec.js +0 -93
- package/components/csrf-token/input.html +0 -5
- package/components/follow-button/follow-button.html +0 -79
- package/components/header-tooltip/index.js +0 -37
- package/components/header-tooltip/main.scss +0 -12
- package/components/unread-articles-indicator/constants.js +0 -4
- package/components/unread-articles-indicator/count-unread-articles.js +0 -26
- package/components/unread-articles-indicator/main.scss +0 -59
- package/components/unread-articles-indicator/tracking.js +0 -15
- package/components/unread-articles-indicator/ui.js +0 -99
- package/components/unread-articles-indicator/update-count.js +0 -40
- package/test/unread-articles-indicator/count-unread-articles.spec.js +0 -72
- package/test/unread-articles-indicator/tracking.spec.js +0 -26
- package/test/unread-articles-indicator/ui.spec.js +0 -123
- package/test/unread-articles-indicator/update-count.spec.js +0 -156
@@ -10,17 +10,9 @@
|
|
10
10
|
{{#concepts}}
|
11
11
|
<li class="collection__concept">
|
12
12
|
{{#if ../liteStyle}}
|
13
|
-
{{
|
14
|
-
variant="primary"
|
15
|
-
buttonText=name
|
16
|
-
collectionName=../collectionName
|
17
|
-
}}
|
13
|
+
{{{renderReactComponent localPath="components/follow-button/follow-button" variant="primary" buttonText=name flags=@root.flags collectionName=../collectionName}}}
|
18
14
|
{{else}}
|
19
|
-
{{
|
20
|
-
variant="inverse"
|
21
|
-
buttonText=name
|
22
|
-
collectionName=../collectionName
|
23
|
-
}}
|
15
|
+
{{{renderReactComponent localPath="components/follow-button/follow-button" variant="inverse" buttonText=name flags=@root.flags collectionName=../collectionName}}}
|
24
16
|
{{/if}}
|
25
17
|
</li>
|
26
18
|
{{/concepts}}
|
@@ -50,7 +42,7 @@
|
|
50
42
|
{{~/unless~}}
|
51
43
|
{{~/concepts~}}"
|
52
44
|
/>
|
53
|
-
{{
|
45
|
+
{{{renderReactComponent localPath="components/csrf-token/input"}}}
|
54
46
|
<input
|
55
47
|
type="hidden"
|
56
48
|
name="name"
|
@@ -20,10 +20,7 @@
|
|
20
20
|
class="concept-list__concept">
|
21
21
|
{{prefLabel}}
|
22
22
|
</a>
|
23
|
-
{{
|
24
|
-
conceptId=id
|
25
|
-
name=prefLabel
|
26
|
-
}}
|
23
|
+
{{{renderReactComponent localPath="components/follow-button/follow-button" conceptId=id name=prefLabel flags=@root.flags}}}
|
27
24
|
</li>
|
28
25
|
{{/each}}
|
29
26
|
</ul>
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import CsrfToken from '../input';
|
3
|
+
import { render } from '@testing-library/react';
|
4
|
+
import '@testing-library/jest-dom';
|
5
|
+
|
6
|
+
const props = {
|
7
|
+
cacheablePersonalisedUrl: false
|
8
|
+
};
|
9
|
+
|
10
|
+
describe('Csrf Token Input', () => {
|
11
|
+
|
12
|
+
test('It renders default button', async () => {
|
13
|
+
let { container } = render(<CsrfToken {...props} />);
|
14
|
+
expect(container.querySelector('[name=\'token\']')).toBeTruthy();
|
15
|
+
});
|
16
|
+
|
17
|
+
test('It renders csrf token attribute', async () => {
|
18
|
+
let { container } = render(<CsrfToken cacheablePersonalisedUrl={true} csrfToken={'test-token'} />);
|
19
|
+
expect(container.querySelector('[data-myft-csrf-token=\'test-token\']')).toBeTruthy();
|
20
|
+
});
|
21
|
+
|
22
|
+
|
23
|
+
});
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
export default function CsrfToken ({ cacheablePersonalisedUrl, csrfToken }) {
|
4
|
+
|
5
|
+
let inputProps = {};
|
6
|
+
|
7
|
+
if (cacheablePersonalisedUrl) {
|
8
|
+
inputProps = {
|
9
|
+
...inputProps,
|
10
|
+
'data-myft-csrf-token': csrfToken
|
11
|
+
};
|
12
|
+
}
|
13
|
+
|
14
|
+
if(csrfToken) {
|
15
|
+
inputProps.value = csrfToken;
|
16
|
+
}
|
17
|
+
|
18
|
+
return (
|
19
|
+
<input
|
20
|
+
{...inputProps}
|
21
|
+
type="hidden"
|
22
|
+
name="token"
|
23
|
+
/>
|
24
|
+
);
|
25
|
+
|
26
|
+
}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import FollowButton from '../follow-button';
|
3
|
+
import { render, screen } from '@testing-library/react';
|
4
|
+
import '@testing-library/jest-dom';
|
5
|
+
|
6
|
+
const props = {
|
7
|
+
flags: {
|
8
|
+
myFtApi: true,
|
9
|
+
myFtApiWrite: true
|
10
|
+
},
|
11
|
+
conceptId: '0000-000000-00000-0000',
|
12
|
+
name: 'Follow button'
|
13
|
+
};
|
14
|
+
|
15
|
+
describe('Follow button', () => {
|
16
|
+
|
17
|
+
test('It renders default button', async () => {
|
18
|
+
render(<FollowButton {...props} />);
|
19
|
+
expect(screen.findByText('Add to myFT')).toBeTruthy();
|
20
|
+
});
|
21
|
+
|
22
|
+
test('It renders a variant', async () => {
|
23
|
+
const { container } = render(<FollowButton {...props} variant={'standard'} />);
|
24
|
+
expect(container.getElementsByClassName('n-myft-follow-button--standard')).toHaveLength(1);
|
25
|
+
});
|
26
|
+
|
27
|
+
test('It renders follow button form', async () => {
|
28
|
+
const { container } = render(<FollowButton {...props} variant={'standard'} />);
|
29
|
+
expect(container.querySelector(`form[action='/myft/add/${props.conceptId}']`)).toBeTruthy();
|
30
|
+
});
|
31
|
+
|
32
|
+
test('Button state changes when attributes change', () => {
|
33
|
+
render(<FollowButton {...props}
|
34
|
+
variant={'standard'}
|
35
|
+
setFollowButtonStateToSelected={true}
|
36
|
+
cacheablePersonalisedUrl={true} />);
|
37
|
+
expect(screen.findByText('Added')).toBeTruthy();
|
38
|
+
});
|
39
|
+
|
40
|
+
});
|
@@ -0,0 +1,174 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import CsrfToken from '../csrf-token/input';
|
3
|
+
|
4
|
+
function generateFormProps (props) {
|
5
|
+
let generatedProps = {};
|
6
|
+
|
7
|
+
const {
|
8
|
+
collectionName,
|
9
|
+
followPlusDigestEmail,
|
10
|
+
conceptId,
|
11
|
+
setFollowButtonStateToSelected,
|
12
|
+
cacheablePersonalisedUrl
|
13
|
+
} = props;
|
14
|
+
|
15
|
+
if (collectionName) {
|
16
|
+
generatedProps['data-myft-tracking'] = `collectionName=${collectionName}`;
|
17
|
+
}
|
18
|
+
|
19
|
+
if(followPlusDigestEmail) {
|
20
|
+
generatedProps['action'] = `/__myft/api/core/follow-plus-digest-email/${conceptId}?method=put`;
|
21
|
+
generatedProps['data-myft-ui-variant'] = 'followPlusDigestEmail';
|
22
|
+
} else {
|
23
|
+
if(setFollowButtonStateToSelected && cacheablePersonalisedUrl) {
|
24
|
+
generatedProps['action'] = `/myft/remove/${conceptId}`;
|
25
|
+
generatedProps['data-js-action'] = `/__myft/api/core/followed/concept/${conceptId}?method=delete`;
|
26
|
+
} else {
|
27
|
+
generatedProps['action'] = `/myft/add/${conceptId}`;
|
28
|
+
generatedProps['data-js-action'] = `/__myft/api/core/followed/concept/${conceptId}?method=put`;
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
return generatedProps;
|
33
|
+
|
34
|
+
}
|
35
|
+
|
36
|
+
function generateButtonProps (props) {
|
37
|
+
|
38
|
+
const {
|
39
|
+
cacheablePersonalisedUrl,
|
40
|
+
setFollowButtonStateToSelected,
|
41
|
+
name,
|
42
|
+
buttonText,
|
43
|
+
variant,
|
44
|
+
conceptId,
|
45
|
+
alternateText,
|
46
|
+
followPlusDigestEmail
|
47
|
+
} = props;
|
48
|
+
|
49
|
+
let generatedProps = {
|
50
|
+
'data-concept-id': conceptId,
|
51
|
+
'n-myft-follow-button': 'true',
|
52
|
+
'data-trackable': 'follow',
|
53
|
+
type: 'submit'
|
54
|
+
};
|
55
|
+
|
56
|
+
if (cacheablePersonalisedUrl && setFollowButtonStateToSelected) {
|
57
|
+
generatedProps['aria-label'] = `Remove ${name} from myFT`;
|
58
|
+
generatedProps['title'] = `Remove ${name} from myFT`
|
59
|
+
generatedProps['data-alternate-label'] = `Add ${name} to myFT`;
|
60
|
+
generatedProps['aria-pressed'] = true;
|
61
|
+
|
62
|
+
if(alternateText) {
|
63
|
+
generatedProps['data-alternate-text'] = alternateText;
|
64
|
+
} else {
|
65
|
+
if(buttonText) {
|
66
|
+
generatedProps['data-alternate-text'] = buttonText;
|
67
|
+
} else {
|
68
|
+
generatedProps['data-alternate-text'] = 'Add to myFT';
|
69
|
+
}
|
70
|
+
}
|
71
|
+
} else {
|
72
|
+
generatedProps['aria-pressed'] = false;
|
73
|
+
generatedProps['aria-label'] = `Add ${name} to myFT`;
|
74
|
+
generatedProps['title'] = `Add ${name} to myFT`;
|
75
|
+
generatedProps['data-alternate-label'] = `Remove ${name} from myFT`;
|
76
|
+
if (alternateText) {
|
77
|
+
generatedProps['data-alternate-text'] = alternateText;
|
78
|
+
} else {
|
79
|
+
if (buttonText) {
|
80
|
+
generatedProps['data-alternate-text'] = buttonText;
|
81
|
+
} else {
|
82
|
+
generatedProps['data-alternate-text'] = 'Added';
|
83
|
+
}
|
84
|
+
}
|
85
|
+
}
|
86
|
+
|
87
|
+
if(variant) {
|
88
|
+
generatedProps[`n-myft-follow-button--${variant}`] = 'true';
|
89
|
+
}
|
90
|
+
|
91
|
+
if(followPlusDigestEmail) {
|
92
|
+
generatedProps['data-trackable-context-messaging'] = 'add-to-myft-plus-digest-button';
|
93
|
+
}
|
94
|
+
|
95
|
+
return generatedProps;
|
96
|
+
}
|
97
|
+
|
98
|
+
function getButtonText (props) {
|
99
|
+
|
100
|
+
const {
|
101
|
+
buttonText,
|
102
|
+
setFollowButtonStateToSelected,
|
103
|
+
cacheablePersonalisedUrl
|
104
|
+
} = props;
|
105
|
+
let outputText;
|
106
|
+
|
107
|
+
if(buttonText) {
|
108
|
+
outputText = buttonText;
|
109
|
+
} else {
|
110
|
+
if(setFollowButtonStateToSelected && cacheablePersonalisedUrl) {
|
111
|
+
outputText = 'Added';
|
112
|
+
} else {
|
113
|
+
outputText = 'Add to myFT';
|
114
|
+
}
|
115
|
+
}
|
116
|
+
|
117
|
+
return outputText;
|
118
|
+
}
|
119
|
+
|
120
|
+
/**
|
121
|
+
*
|
122
|
+
* @param {Object} props
|
123
|
+
* @param {string} props.name
|
124
|
+
* @param {Object} props.flags
|
125
|
+
* @param {string} props.extraClasses
|
126
|
+
* @param {string} props.conceptId
|
127
|
+
* @param {string} props.variant
|
128
|
+
* @param {string} props.buttonText
|
129
|
+
* @param {*} props.setFollowButtonStateToSelected
|
130
|
+
* @param {string} props.cacheablePersonalisedUrl
|
131
|
+
* @param {string} props.alternateText
|
132
|
+
* @param {*} props.followPlusDigestEmail
|
133
|
+
* @param {string} props.collectionName
|
134
|
+
*/
|
135
|
+
export default function FollowButton (props) {
|
136
|
+
|
137
|
+
const {
|
138
|
+
name,
|
139
|
+
flags,
|
140
|
+
extraClasses,
|
141
|
+
conceptId,
|
142
|
+
variant,
|
143
|
+
} = props;
|
144
|
+
|
145
|
+
const formProps = generateFormProps(props);
|
146
|
+
const buttonProps = generateButtonProps(props);
|
147
|
+
|
148
|
+
const getVariantClass = (variant) => variant ? `n-myft-follow-button--${variant}` : '';
|
149
|
+
|
150
|
+
return (
|
151
|
+
<>
|
152
|
+
{flags.myFtApiWrite && <form
|
153
|
+
className={`n-myft-ui n-myft-ui--follow ${extraClasses || ''}`}
|
154
|
+
method="GET"
|
155
|
+
data-myft-ui="follow"
|
156
|
+
data-concept-id={conceptId}
|
157
|
+
{...formProps}>
|
158
|
+
<CsrfToken cacheablePersonalisedUrl={props.cacheablePersonalisedUrl} csrfToken={props.csrfToken} />
|
159
|
+
<div
|
160
|
+
className="n-myft-ui__announcement o-normalise-visually-hidden"
|
161
|
+
aria-live="assertive"
|
162
|
+
data-pressed-text={`Now following ${name}.`}
|
163
|
+
data-unpressed-text={`No longer following ${name}.`}
|
164
|
+
></div>
|
165
|
+
<button
|
166
|
+
{...buttonProps}
|
167
|
+
className={[`n-myft-follow-button ${getVariantClass(variant)}`]}>
|
168
|
+
{getButtonText(props)}
|
169
|
+
</button>
|
170
|
+
</form>}
|
171
|
+
</>
|
172
|
+
);
|
173
|
+
|
174
|
+
}
|
@@ -5,7 +5,7 @@
|
|
5
5
|
data-concept-id="{{conceptId}}"
|
6
6
|
action="/myft/add/{{conceptId}}?instant=true"
|
7
7
|
data-js-action="/__myft/api/core/followed/concept/{{conceptId}}?method=put">
|
8
|
-
{{
|
8
|
+
{{{renderReactComponent localPath="components/csrf-token/input"}}}
|
9
9
|
<input type="hidden" value="{{name}}" name="name">
|
10
10
|
{{#if directType}}
|
11
11
|
<input type="hidden" value="{{directType}}" name="directType">
|
@@ -2,7 +2,7 @@
|
|
2
2
|
<span class="myft-pin-divider"></span>
|
3
3
|
<div class="myft-pin-button-wrapper">
|
4
4
|
<form method="post" action="/__myft/api/core/prioritised/concept/{{id}}?method={{#if prioritised}}delete{{else}}put{{/if}}" data-myft-prioritise>
|
5
|
-
{{
|
5
|
+
{{{renderReactComponent localPath="components/csrf-token/input"}}}
|
6
6
|
<input type="hidden" value="{{name}}" name="name"> {{#if directType}}
|
7
7
|
<input type="hidden" value="{{directType}}" name="directType"> {{else}}
|
8
8
|
<input type="hidden" value="http://www.ft.com/ontology/concept/Concept" name="directType"> {{/if}}
|
@@ -4,7 +4,7 @@
|
|
4
4
|
data-myft-ui="saved"
|
5
5
|
action="/myft/save/{{contentId}}"
|
6
6
|
data-js-action="/__myft/api/core/saved/content/{{contentId}}?method=put">
|
7
|
-
{{
|
7
|
+
{{{renderReactComponent localPath="components/csrf-token/input"}}}
|
8
8
|
<div
|
9
9
|
class="n-myft-ui__announcement o-normalise-visually-hidden"
|
10
10
|
aria-live="assertive"
|
@@ -17,10 +17,14 @@
|
|
17
17
|
data-trackable="{{#if trackableId}}{{trackableId}}{{else}}save-for-later{{/if}}"
|
18
18
|
{{#if isSaved}}
|
19
19
|
{{!-- The value of alternate label needs to be the opposite of label / the current saved state. This allows the client side JS to toggle the labels on state changes --}}
|
20
|
+
title="{{#if title}}{{title}} is{{/if}} Saved to myFT"
|
21
|
+
aria-label="{{#if title}}{{title}} is{{/if}} Saved to myFT"
|
20
22
|
data-alternate-label="{{#if title}}Save {{title}} to myFT for later{{else}}Save this article to myFT for later{{/if}}"
|
21
23
|
aria-pressed="true"
|
22
24
|
{{else}}
|
23
|
-
|
25
|
+
title="{{#if title}}Save {{title}} to myFT for later{{else}}Save this article to myFT for later{{/if}}"
|
26
|
+
aria-label="{{#if title}}Save {{title}} to myFT for later{{else}}Save this article to myFT for later{{/if}}"
|
27
|
+
data-alternate-label="{{#if title}}{{title}} is{{/if}} Saved to myFT"
|
24
28
|
aria-pressed="false"
|
25
29
|
{{/if}}
|
26
30
|
{{#unlessEquals appIsStreamPage true}}
|
@@ -40,13 +44,6 @@
|
|
40
44
|
{{/if}}
|
41
45
|
{{/if}}
|
42
46
|
data-content-id="{{contentId}}" {{! duplicated here for tracking}}
|
43
|
-
{{#if isSaved}}
|
44
|
-
aria-label="{{#if title}}{{title}} is{{/if}} Saved to myFT"
|
45
|
-
title="{{#if title}}{{title}} is{{/if}} Saved to myFT"
|
46
|
-
{{else}}
|
47
|
-
aria-label="{{#if title}}Save {{title}} to myFT for later{{else}}Save this article to myFT for later{{/if}}"
|
48
|
-
title="{{#if title}}Save {{title}} to myFT for later{{else}}Save this article to myFT for later{{/if}}"
|
49
|
-
{{/if}}
|
50
47
|
>
|
51
48
|
{{#if saveButtonWithIcon}}
|
52
49
|
<span class="save-button-with-icon-copy" data-variant-label>{{#if buttonText~}}
|
@@ -1,65 +1,5 @@
|
|
1
1
|
# n-myft-ui/unread-articles-indicator
|
2
2
|
|
3
|
-
|
3
|
+
Note: this component is decommissioned but a small function (`getNewArticlesSinceTime`) of it is still in use by next-myft-page to display a "New" label under an article in the Timeline view only.
|
4
4
|
|
5
|
-
|
6
|
-
## What?
|
7
|
-
|
8
|
-
It indicates to the user that articles they've not yet read have been published to their myft feed since they last visited the site.
|
9
|
-
|
10
|
-
## Why?
|
11
|
-
|
12
|
-
- To encourage a myFT feed habit
|
13
|
-
- To provide a utility to the user so they don't need to check their feed unnecessarily
|
14
|
-
|
15
|
-
|
16
|
-
## Where?
|
17
|
-
|
18
|
-
- **.com only**:
|
19
|
-
- next-front-page
|
20
|
-
- next-stream-page
|
21
|
-
- next-article
|
22
|
-
- next-myft-page
|
23
|
-
|
24
|
-
## When?
|
25
|
-
|
26
|
-
- June 2018 - intial implementation [AB Test results](https://sites.google.com/ft.com/ftproductanalytics/analysis/myft/myft-feed-page)
|
27
|
-
- Sep 2018 - sync across devices
|
28
|
-
- Oct 2019 - add favicon [AB Test results](https://sites.google.com/ft.com/ftproductanalytics/tests/engagement/myftunreadfavicon)
|
29
|
-
- Nov 2019 - automatically update (polling)
|
30
|
-
|
31
|
-
|
32
|
-
## Behaviour
|
33
|
-
|
34
|
-
- The indicator consists of a red circle and a count of the number of new and unread articles since a user was last active on the site (or the app). The article count is hidden on the mobile breakpoint if the number is double digits (i.e. too large to fit in the circle)
|
35
|
-
|
36
|
-
- The favicon is updated to reflect what the indicator is showing
|
37
|
-
|
38
|
-
- The article count updates automatically without the user having to refresh the page every 5 minutes
|
39
|
-
|
40
|
-
- The count is reset to 0 when the user visits their myFT feed page on [ft.com](https://www.ft.com/ft.com/myft/following)
|
41
|
-
|
42
|
-
- The count is reset every day at midnight
|
43
|
-
|
44
|
-
- The count is reset after 30 minutes of site inactivity - 🐛 thought to be a bug
|
45
|
-
|
46
|
-
- The state of the indicator is synched between different browsers, however it's not expected to be real-time
|
47
|
-
---
|
48
|
-
|
49
|
-
## How the unread articles number is determined
|
50
|
-
|
51
|
-
:one: Determine the time(`feedStartTime`) which is the user's last active time (the process is explained below)
|
52
|
-
|
53
|
-
:two: Determine the time(`startTime`) to be used to count unread articles for the current visit. It is `feedStartTime` or `myFTIndicatorDismissedAt`*, whichever comes later.
|
54
|
-
|
55
|
-
:three: Fetch myft feed articles for the user
|
56
|
-
|
57
|
-
:four: Count the articles published after the timestamp(`startTime`)
|
58
|
-
|
59
|
-
---
|
60
|
-
* `myFTIndicatorDismissedAt` timestamp is stored in `window.localStorage` when user visits [myFT feed page](https://www.ft.com/myft/following).
|
61
|
-
|
62
|
-
---
|
63
|
-
![unread article count](https://user-images.githubusercontent.com/21194161/72608180-11df4800-391a-11ea-973b-4a52933561ab.png)
|
64
|
-
|
65
|
-
To keep the number synched on **cross devices**, it fetches the last 'active' time for a user via a Volt Procedure called UserInfo.
|
5
|
+
All other previous functionality of the component has been removed.
|
@@ -1,5 +1,5 @@
|
|
1
1
|
// date-fns from v2 doesn't accept String arguments anymore.
|
2
|
-
// the detail => https://github.com/date-fns/date-fns/blob/
|
2
|
+
// the detail => https://github.com/date-fns/date-fns/blob/HEAD/CHANGELOG.md#200---2019-08-20
|
3
3
|
// By adding validation for dates before their functions allows us to know it when unexpected value passed.
|
4
4
|
|
5
5
|
import isTodayOriginal from 'date-fns/src/isToday';
|
@@ -11,22 +11,15 @@ import parseISO from 'date-fns/src/parseISO';
|
|
11
11
|
|
12
12
|
const isValid = (date) => {
|
13
13
|
if (!isValidOriginal(date)) {
|
14
|
-
console.error(
|
14
|
+
console.error("Invalid date passed", [date]); //eslint-disable-line
|
15
15
|
}
|
16
16
|
return date;
|
17
17
|
};
|
18
18
|
|
19
19
|
const isToday = (date) => isTodayOriginal(isValid(date));
|
20
|
-
const isAfter = (date, dateToCompare) =>
|
20
|
+
const isAfter = (date, dateToCompare) =>
|
21
|
+
isAfterOriginal(isValid(date), isValid(dateToCompare));
|
21
22
|
const addMinutes = (date, amount) => addMinutesOriginal(isValid(date), amount);
|
22
23
|
const startOfDay = (date) => startOfDayOriginal(isValid(date));
|
23
24
|
|
24
|
-
|
25
|
-
export {
|
26
|
-
isToday,
|
27
|
-
isAfter,
|
28
|
-
addMinutes,
|
29
|
-
startOfDay,
|
30
|
-
isValid,
|
31
|
-
parseISO,
|
32
|
-
};
|
25
|
+
export { isToday, isAfter, addMinutes, startOfDay, isValid, parseISO };
|
@@ -1,42 +1,11 @@
|
|
1
1
|
import {startOfDay} from './date-fns';
|
2
2
|
import * as storage from './storage';
|
3
|
-
import * as ui from './ui';
|
4
|
-
import updateCount from './update-count';
|
5
3
|
import initialiseFeedStartTime from './initialise-feed-start-time';
|
6
4
|
import sessionClient from 'next-session-client';
|
7
|
-
import {UPDATE_INTERVAL} from './constants';
|
8
5
|
|
9
|
-
let shouldPoll;
|
10
|
-
let updateTimeout;
|
11
6
|
let initialFeedStartTime;
|
12
7
|
let userId;
|
13
8
|
|
14
|
-
const isMyftFeedPage = window.location.pathname.indexOf('/myft/following') === 0;
|
15
|
-
const doUpdate = () => updater().catch(stopPolling);
|
16
|
-
|
17
|
-
export default async (options = {}) => {
|
18
|
-
try {
|
19
|
-
if (!storage.isAvailable()) return;
|
20
|
-
|
21
|
-
const myftHeaderLink = document.querySelectorAll('.o-header__top-link--myft');
|
22
|
-
const uiOpts = Object.assign({onClick: setDismissed, flags: {}}, options);
|
23
|
-
shouldPoll = uiOpts.flags.MyFT_UnreadArticlesIndicatorPolling;
|
24
|
-
|
25
|
-
await getNewArticlesSinceTime();
|
26
|
-
|
27
|
-
const {count = 0} = storage.getLastUpdate() || {};
|
28
|
-
ui.createIndicators(myftHeaderLink, uiOpts);
|
29
|
-
ui.setCount(count);
|
30
|
-
|
31
|
-
document.addEventListener('visibilitychange', onVisibilityChange);
|
32
|
-
storage.addCountChangeListeners(newCount => ui.setCount(newCount));
|
33
|
-
if (isMyftFeedPage) setDismissed();
|
34
|
-
return updater();
|
35
|
-
} catch(e) {
|
36
|
-
|
37
|
-
}
|
38
|
-
};
|
39
|
-
|
40
9
|
async function getValidSession () {
|
41
10
|
if (!userId) {
|
42
11
|
const {uuid} = await sessionClient.uuid();
|
@@ -47,6 +16,7 @@ async function getValidSession () {
|
|
47
16
|
}
|
48
17
|
|
49
18
|
// Export used in next-myft -page to determine whether to add "New" label to articles in feed
|
19
|
+
//KEEP: This function is in use in next-myft-page do not delete!
|
50
20
|
export async function getNewArticlesSinceTime () {
|
51
21
|
const user = await getValidSession();
|
52
22
|
const dayStart = startOfDay(new Date());
|
@@ -59,34 +29,3 @@ export async function getNewArticlesSinceTime () {
|
|
59
29
|
|
60
30
|
return initialFeedStartTime || dayStart;
|
61
31
|
}
|
62
|
-
|
63
|
-
async function updater () {
|
64
|
-
const user = await getValidSession();
|
65
|
-
await updateCount(user, new Date());
|
66
|
-
if (!shouldPoll) return;
|
67
|
-
updateTimeout = window.setTimeout(doUpdate, UPDATE_INTERVAL);
|
68
|
-
}
|
69
|
-
|
70
|
-
async function onVisibilityChange () {
|
71
|
-
if (document.visibilityState !== 'visible') return;
|
72
|
-
try {
|
73
|
-
await getValidSession();
|
74
|
-
await getNewArticlesSinceTime();
|
75
|
-
if (updateTimeout) window.clearTimeout(updateTimeout);
|
76
|
-
await updater();
|
77
|
-
} catch(e) {
|
78
|
-
stopPolling();
|
79
|
-
}
|
80
|
-
}
|
81
|
-
|
82
|
-
function stopPolling () {
|
83
|
-
userId = undefined;
|
84
|
-
if (updateTimeout) {
|
85
|
-
window.clearTimeout(updateTimeout);
|
86
|
-
}
|
87
|
-
}
|
88
|
-
|
89
|
-
function setDismissed () {
|
90
|
-
storage.updateLastUpdate({count: 0, time: new Date()});
|
91
|
-
storage.setIndicatorDismissedTime(new Date());
|
92
|
-
}
|
@@ -2,10 +2,7 @@ import {isValid} from './date-fns';
|
|
2
2
|
|
3
3
|
const DEVICE_SESSION_EXPIRY = 'deviceSessionExpiry';
|
4
4
|
const FEED_START_TIME = 'newArticlesSinceTime';
|
5
|
-
const LAST_INDICATOR_UPDATE = 'myFTIndicatorUpdate';
|
6
|
-
const INDICATOR_DISMISSED_TIME = 'myFTIndicatorDismissedAt';
|
7
5
|
|
8
|
-
const countChangeListeners = new Set();
|
9
6
|
const isISOString = str => typeof str === 'string' && str.charAt(10) === 'T';
|
10
7
|
const getStoredDate = key => {
|
11
8
|
const value = window.localStorage.getItem(key);
|
@@ -13,7 +10,6 @@ const getStoredDate = key => {
|
|
13
10
|
|
14
11
|
return isISOString(value) && isValid(date) ? date : null;
|
15
12
|
};
|
16
|
-
let lastCount;
|
17
13
|
|
18
14
|
export const getDeviceSessionExpiry = () => getStoredDate(DEVICE_SESSION_EXPIRY);
|
19
15
|
|
@@ -23,10 +19,6 @@ export const getFeedStartTime = () => getStoredDate(FEED_START_TIME);
|
|
23
19
|
|
24
20
|
export const setFeedStartTime = date => window.localStorage.setItem(FEED_START_TIME, date.toISOString());
|
25
21
|
|
26
|
-
export const getIndicatorDismissedTime = () => getStoredDate(INDICATOR_DISMISSED_TIME);
|
27
|
-
|
28
|
-
export const setIndicatorDismissedTime = date => window.localStorage.setItem(INDICATOR_DISMISSED_TIME, date.toISOString());
|
29
|
-
|
30
22
|
export const isAvailable = () => {
|
31
23
|
try {
|
32
24
|
const storage = window.localStorage;
|
@@ -39,39 +31,3 @@ export const isAvailable = () => {
|
|
39
31
|
return false;
|
40
32
|
}
|
41
33
|
};
|
42
|
-
|
43
|
-
|
44
|
-
export const setLastUpdate = (update = {}) => {
|
45
|
-
const toStore = Object.assign({}, update);
|
46
|
-
if (update.time) toStore.time = update.time.toISOString();
|
47
|
-
if (update.updateStarted) toStore.updateStarted = update.updateStarted.toISOString();
|
48
|
-
window.localStorage.setItem(LAST_INDICATOR_UPDATE, JSON.stringify(toStore));
|
49
|
-
fireListeners();
|
50
|
-
};
|
51
|
-
|
52
|
-
export const getLastUpdate = () => {
|
53
|
-
try {
|
54
|
-
const lastUpdate = JSON.parse(window.localStorage.getItem(LAST_INDICATOR_UPDATE));
|
55
|
-
if (lastUpdate.time) lastUpdate.time = new Date(lastUpdate.time);
|
56
|
-
if (lastUpdate.updateStarted) lastUpdate.updateStarted = new Date(lastUpdate.updateStarted);
|
57
|
-
return lastUpdate;
|
58
|
-
} catch (e) {}
|
59
|
-
};
|
60
|
-
|
61
|
-
export const updateLastUpdate = (update) => setLastUpdate(Object.assign({}, getLastUpdate(), update) );
|
62
|
-
|
63
|
-
export const addCountChangeListeners = listener => {
|
64
|
-
if (!countChangeListeners.size) {
|
65
|
-
window.addEventListener('storage', fireListeners);
|
66
|
-
}
|
67
|
-
countChangeListeners.add(listener);
|
68
|
-
};
|
69
|
-
|
70
|
-
function fireListeners () {
|
71
|
-
const {count} = getLastUpdate() || {};
|
72
|
-
if (count === lastCount) return;
|
73
|
-
for (const listener of countChangeListeners) {
|
74
|
-
listener(count || 0);
|
75
|
-
}
|
76
|
-
lastCount = count || 0;
|
77
|
-
}
|
package/demos/app.js
CHANGED
@@ -1,9 +1,17 @@
|
|
1
|
+
require('sucrase/register');
|
1
2
|
const express = require('@financial-times/n-internal-tool');
|
2
3
|
const chalk = require('chalk');
|
3
4
|
const errorHighlight = chalk.bold.red;
|
4
5
|
const highlight = chalk.bold.green;
|
5
|
-
|
6
|
+
const { PageKitReactJSX } = require('@financial-times/dotcom-server-react-jsx');
|
7
|
+
const fs = require('fs');
|
8
|
+
const path = require('path');
|
6
9
|
const xHandlebars = require('@financial-times/x-handlebars');
|
10
|
+
const handlebars = require('handlebars');
|
11
|
+
const { helpers } = require('@financial-times/dotcom-server-handlebars');
|
12
|
+
|
13
|
+
const demoJSX = require('./templates/demo').default;
|
14
|
+
const demoLayoutSource = fs.readFileSync(path.join(__dirname, './templates/demo-layout.html'),'utf8').toString();
|
7
15
|
|
8
16
|
const fixtures = {
|
9
17
|
followButton: require('./fixtures/follow-button'),
|
@@ -29,10 +37,13 @@ const app = module.exports = express({
|
|
29
37
|
demo: true,
|
30
38
|
s3o: false,
|
31
39
|
helpers: {
|
32
|
-
x: xHandlebars()
|
33
|
-
|
40
|
+
x: xHandlebars(),
|
41
|
+
renderReactComponent: helpers.renderReactComponent
|
42
|
+
},
|
34
43
|
});
|
35
44
|
|
45
|
+
const jsxRenderer = (new PageKitReactJSX({ includeDoctype: false }));
|
46
|
+
|
36
47
|
app.get('/', (req, res) => {
|
37
48
|
res.render('demo', Object.assign({
|
38
49
|
title: 'n-myft-ui demo',
|
@@ -44,6 +55,22 @@ app.get('/', (req, res) => {
|
|
44
55
|
}, fixtures));
|
45
56
|
});
|
46
57
|
|
58
|
+
app.get('/demo-jsx', async (req, res) => {
|
59
|
+
let demo = await jsxRenderer.render(demoJSX, Object.assign({
|
60
|
+
title: 'n-myft-ui demo',
|
61
|
+
layout: 'demo-layout',
|
62
|
+
flags: {
|
63
|
+
myFtApi: true,
|
64
|
+
myFtApiWrite: true
|
65
|
+
}
|
66
|
+
}, fixtures));
|
67
|
+
|
68
|
+
let template = handlebars.compile(demoLayoutSource);
|
69
|
+
let result = template({body: demo});
|
70
|
+
|
71
|
+
res.send(result);
|
72
|
+
});
|
73
|
+
|
47
74
|
app.get('/digest-on-follow', (req, res) => {
|
48
75
|
res.render('digest-on-follow', Object.assign({
|
49
76
|
title: 'n-myft-ui digest on follow',
|