@pushwoosh/dumb-components 1.1.221 → 1.1.223
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EmailTemplatePreview/__tests__/clickMap.test.js +33 -2
- package/EmailTemplatePreview/clickMap.d.ts +3 -1
- package/EmailTemplatePreview/clickMap.js +14 -3
- package/EmailTemplatePreview/types.d.ts +2 -0
- package/InboxPreview/InboxPreview.d.ts +13 -0
- package/InboxPreview/InboxPreview.js +82 -0
- package/InboxPreview/__tests__/helpers.test.d.ts +1 -0
- package/InboxPreview/__tests__/helpers.test.js +201 -0
- package/InboxPreview/components/InboxAndroidCell/InboxAndroidCell.d.ts +4 -0
- package/InboxPreview/components/InboxAndroidCell/InboxAndroidCell.js +70 -0
- package/InboxPreview/components/InboxAndroidCell/index.d.ts +1 -0
- package/InboxPreview/components/InboxAndroidCell/index.js +1 -0
- package/InboxPreview/components/InboxCellAvatar/InboxCellAvatar.d.ts +4 -0
- package/InboxPreview/components/InboxCellAvatar/InboxCellAvatar.js +48 -0
- package/InboxPreview/components/InboxCellAvatar/index.d.ts +1 -0
- package/InboxPreview/components/InboxCellAvatar/index.js +1 -0
- package/InboxPreview/components/InboxCellAvatar/styles.d.ts +1 -0
- package/InboxPreview/components/InboxCellAvatar/styles.js +5 -0
- package/InboxPreview/components/InboxCellAvatar/types.d.ts +10 -0
- package/InboxPreview/components/InboxCellAvatar/types.js +1 -0
- package/InboxPreview/components/InboxCellCarousel/InboxCellCarousel.d.ts +4 -0
- package/InboxPreview/components/InboxCellCarousel/InboxCellCarousel.js +53 -0
- package/InboxPreview/components/InboxCellCarousel/index.d.ts +1 -0
- package/InboxPreview/components/InboxCellCarousel/index.js +1 -0
- package/InboxPreview/components/InboxCellCarousel/styles.d.ts +29 -0
- package/InboxPreview/components/InboxCellCarousel/styles.js +24 -0
- package/InboxPreview/components/InboxCellCarousel/types.d.ts +8 -0
- package/InboxPreview/components/InboxCellCarousel/types.js +1 -0
- package/InboxPreview/components/InboxCellMedia/InboxCellMedia.d.ts +4 -0
- package/InboxPreview/components/InboxCellMedia/InboxCellMedia.js +26 -0
- package/InboxPreview/components/InboxCellMedia/index.d.ts +1 -0
- package/InboxPreview/components/InboxCellMedia/index.js +1 -0
- package/InboxPreview/components/InboxCellMedia/types.d.ts +12 -0
- package/InboxPreview/components/InboxCellMedia/types.js +1 -0
- package/InboxPreview/components/InboxIOSCell/InboxIOSCell.d.ts +4 -0
- package/InboxPreview/components/InboxIOSCell/InboxIOSCell.js +125 -0
- package/InboxPreview/components/InboxIOSCell/index.d.ts +1 -0
- package/InboxPreview/components/InboxIOSCell/index.js +1 -0
- package/InboxPreview/components/InboxWebCell/InboxWebCell.d.ts +4 -0
- package/InboxPreview/components/InboxWebCell/InboxWebCell.js +83 -0
- package/InboxPreview/components/InboxWebCell/index.d.ts +1 -0
- package/InboxPreview/components/InboxWebCell/index.js +1 -0
- package/InboxPreview/constants.d.ts +14 -0
- package/InboxPreview/constants.js +30 -0
- package/InboxPreview/helpers.d.ts +29 -0
- package/InboxPreview/helpers.js +75 -0
- package/InboxPreview/index.d.ts +4 -0
- package/InboxPreview/index.js +3 -0
- package/InboxPreview/polyglot/index.d.ts +2 -0
- package/InboxPreview/polyglot/index.js +34 -0
- package/InboxPreview/polyglot/messages_ar.d.ts +3 -0
- package/InboxPreview/polyglot/messages_ar.js +4 -0
- package/InboxPreview/polyglot/messages_bn.d.ts +3 -0
- package/InboxPreview/polyglot/messages_bn.js +4 -0
- package/InboxPreview/polyglot/messages_de.d.ts +3 -0
- package/InboxPreview/polyglot/messages_de.js +4 -0
- package/InboxPreview/polyglot/messages_en.d.ts +3 -0
- package/InboxPreview/polyglot/messages_en.js +4 -0
- package/InboxPreview/polyglot/messages_es.d.ts +3 -0
- package/InboxPreview/polyglot/messages_es.js +4 -0
- package/InboxPreview/polyglot/messages_fr.d.ts +3 -0
- package/InboxPreview/polyglot/messages_fr.js +4 -0
- package/InboxPreview/polyglot/messages_he.d.ts +3 -0
- package/InboxPreview/polyglot/messages_he.js +4 -0
- package/InboxPreview/polyglot/messages_hi.d.ts +3 -0
- package/InboxPreview/polyglot/messages_hi.js +4 -0
- package/InboxPreview/polyglot/messages_ja.d.ts +3 -0
- package/InboxPreview/polyglot/messages_ja.js +4 -0
- package/InboxPreview/polyglot/messages_ko.d.ts +3 -0
- package/InboxPreview/polyglot/messages_ko.js +4 -0
- package/InboxPreview/polyglot/messages_pt.d.ts +3 -0
- package/InboxPreview/polyglot/messages_pt.js +4 -0
- package/InboxPreview/polyglot/messages_ru.d.ts +3 -0
- package/InboxPreview/polyglot/messages_ru.js +4 -0
- package/InboxPreview/polyglot/messages_th.d.ts +3 -0
- package/InboxPreview/polyglot/messages_th.js +4 -0
- package/InboxPreview/polyglot/messages_zh.d.ts +3 -0
- package/InboxPreview/polyglot/messages_zh.js +4 -0
- package/InboxPreview/polyglot/types.d.ts +5 -0
- package/InboxPreview/polyglot/types.js +1 -0
- package/InboxPreview/styles.d.ts +25 -0
- package/InboxPreview/styles.js +21 -0
- package/InboxPreview/types.d.ts +74 -0
- package/InboxPreview/types.js +1 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CLICK_MAP_BADGE_ATTRIBUTE, decorateClickMap, matchClickMapItems } from '../clickMap';
|
|
2
2
|
const PROMO = 'http://spartak.com/matchtowin';
|
|
3
3
|
const TICKETS = 'https://tickets.spartak.com/zones/18956';
|
|
4
|
+
const SHOP = 'https://shop.spartak.com/';
|
|
4
5
|
const HTML = `
|
|
5
6
|
<a href="${PROMO}?utm_source=email&utm_content=img"><img src="banner.png" alt=""></a>
|
|
6
7
|
<p>Подробности на <a href="${PROMO}?utm_source=email">странице акции</a>.</p>
|
|
@@ -8,12 +9,15 @@ const HTML = `
|
|
|
8
9
|
<a href="${TICKETS}?utm_source=email">Купить билеты</a>
|
|
9
10
|
<a href="${TICKETS}?utm_source=email">Купить билеты</a>
|
|
10
11
|
<a href="[% param.url_unsub %]">Отписаться</a>
|
|
12
|
+
<a data-pw-lid="aaaa11112222" href="${SHOP}">В магазин</a>
|
|
13
|
+
<a data-pw-lid="bbbb33334444" href="${SHOP}">В магазин</a>
|
|
11
14
|
`;
|
|
12
|
-
const item = (key, link, title, clicks) => ({
|
|
15
|
+
const item = (key, link, title, clicks, linkId) => ({
|
|
13
16
|
key,
|
|
14
17
|
link,
|
|
15
18
|
title,
|
|
16
|
-
clicks
|
|
19
|
+
clicks,
|
|
20
|
+
linkId
|
|
17
21
|
});
|
|
18
22
|
function createDocument() {
|
|
19
23
|
const frameDocument = document.implementation.createHTMLDocument('preview');
|
|
@@ -64,6 +68,33 @@ describe('matchClickMapItems', () => {
|
|
|
64
68
|
expect(placement.pooled).toEqual(['promo']);
|
|
65
69
|
expect(matches).toHaveLength(3);
|
|
66
70
|
});
|
|
71
|
+
it('tells identical anchors apart by the stamped link id', () => {
|
|
72
|
+
const {
|
|
73
|
+
matches,
|
|
74
|
+
placement
|
|
75
|
+
} = matchClickMapItems(createDocument(), [item('top', SHOP, 'В магазин', 40, 'aaaa11112222'), item('bottom', SHOP, 'В магазин', 9, 'bbbb33334444')]);
|
|
76
|
+
expect(placement).toEqual({
|
|
77
|
+
placed: ['top', 'bottom'],
|
|
78
|
+
pooled: [],
|
|
79
|
+
unplaced: []
|
|
80
|
+
});
|
|
81
|
+
expect(matches.map(({
|
|
82
|
+
anchor,
|
|
83
|
+
items: matched
|
|
84
|
+
}) => [anchor.getAttribute('data-pw-lid'), matched.map(row => row.key)])).toEqual([['aaaa11112222', ['top']], ['bbbb33334444', ['bottom']]]);
|
|
85
|
+
});
|
|
86
|
+
it('falls back to link and text when the template no longer carries the recorded link id', () => {
|
|
87
|
+
const {
|
|
88
|
+
matches,
|
|
89
|
+
placement
|
|
90
|
+
} = matchClickMapItems(createDocument(), [item('old', SHOP, 'В магазин', 3, 'cccc55556666'), item('unstamped', PROMO, 'странице акции', 1)]);
|
|
91
|
+
expect(placement).toEqual({
|
|
92
|
+
placed: ['unstamped'],
|
|
93
|
+
pooled: ['old'],
|
|
94
|
+
unplaced: []
|
|
95
|
+
});
|
|
96
|
+
expect(matches).toHaveLength(3);
|
|
97
|
+
});
|
|
67
98
|
it('reports rows whose link is not in the template as unplaced', () => {
|
|
68
99
|
const {
|
|
69
100
|
matches,
|
|
@@ -19,7 +19,9 @@ export type EmailClickMapDecorateOptions = {
|
|
|
19
19
|
};
|
|
20
20
|
/** Attribute every badge carries, so tests and consumers can find them. */
|
|
21
21
|
export declare const CLICK_MAP_BADGE_ATTRIBUTE = "data-pw-click-map-badge";
|
|
22
|
-
/**
|
|
22
|
+
/** Stable anchor id stamped into the template html on save; the click carries it as `linkId`. */
|
|
23
|
+
export declare const CLICK_MAP_LINK_ID_ATTRIBUTE = "data-pw-lid";
|
|
24
|
+
/** Lands click-tracking rows on template anchors by stamped `linkId`, else by (link, text), else every anchor of the link. */
|
|
23
25
|
export declare function matchClickMapItems(document: Document, items: EmailClickMapItem[]): EmailClickMapMatchResult;
|
|
24
26
|
/** Draws click-count badges over matched anchors, outlines `activeKey`; returns a cleanup. */
|
|
25
27
|
export declare function decorateClickMap(document: Document, options: EmailClickMapDecorateOptions): () => void;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** Attribute every badge carries, so tests and consumers can find them. */
|
|
2
2
|
export const CLICK_MAP_BADGE_ATTRIBUTE = 'data-pw-click-map-badge';
|
|
3
|
+
/** Stable anchor id stamped into the template html on save; the click carries it as `linkId`. */
|
|
4
|
+
export const CLICK_MAP_LINK_ID_ATTRIBUTE = 'data-pw-lid';
|
|
3
5
|
const ACTIVE_COLOR = '#00b2ff';
|
|
4
6
|
const IDLE_COLOR = 'rgba(36, 48, 66, 0.82)';
|
|
5
7
|
const compactNumber = new Intl.NumberFormat('en', {
|
|
@@ -10,14 +12,19 @@ const stripQuery = url => url.split('?')[0].trim();
|
|
|
10
12
|
const normalizeText = text => text.replace(/\s+/g, ' ').trim();
|
|
11
13
|
const anchorText = anchor => normalizeText(anchor.textContent ?? '');
|
|
12
14
|
const anchorLink = anchor => stripQuery(anchor.getAttribute('href') ?? '');
|
|
13
|
-
/** Lands click-tracking rows on template anchors by (link, text),
|
|
15
|
+
/** Lands click-tracking rows on template anchors by stamped `linkId`, else by (link, text), else every anchor of the link. */
|
|
14
16
|
export function matchClickMapItems(document, items) {
|
|
15
17
|
const anchorsByLink = new Map();
|
|
18
|
+
const anchorsByLinkId = new Map();
|
|
16
19
|
document.querySelectorAll('a[href]').forEach(anchor => {
|
|
17
20
|
const link = anchorLink(anchor);
|
|
21
|
+
const linkId = anchor.getAttribute(CLICK_MAP_LINK_ID_ATTRIBUTE);
|
|
18
22
|
if (link) {
|
|
19
23
|
anchorsByLink.set(link, [...(anchorsByLink.get(link) ?? []), anchor]);
|
|
20
24
|
}
|
|
25
|
+
if (linkId) {
|
|
26
|
+
anchorsByLinkId.set(linkId, [...(anchorsByLinkId.get(linkId) ?? []), anchor]);
|
|
27
|
+
}
|
|
21
28
|
});
|
|
22
29
|
const itemsByAnchor = new Map();
|
|
23
30
|
const placement = {
|
|
@@ -25,11 +32,15 @@ export function matchClickMapItems(document, items) {
|
|
|
25
32
|
pooled: [],
|
|
26
33
|
unplaced: []
|
|
27
34
|
};
|
|
28
|
-
|
|
35
|
+
const targetsByLinkAndText = item => {
|
|
29
36
|
const candidates = anchorsByLink.get(stripQuery(item.link)) ?? [];
|
|
30
37
|
const title = normalizeText(item.title);
|
|
31
38
|
const exact = candidates.filter(anchor => anchorText(anchor) === title);
|
|
32
|
-
|
|
39
|
+
return exact.length > 0 ? exact : candidates;
|
|
40
|
+
};
|
|
41
|
+
items.forEach(item => {
|
|
42
|
+
const stamped = item.linkId ? anchorsByLinkId.get(item.linkId) : undefined;
|
|
43
|
+
const targets = stamped ?? targetsByLinkAndText(item);
|
|
33
44
|
if (targets.length === 0) {
|
|
34
45
|
placement.unplaced.push(item.key);
|
|
35
46
|
return;
|
|
@@ -6,6 +6,8 @@ export type EmailClickMapItem = {
|
|
|
6
6
|
link: string;
|
|
7
7
|
/** Anchor text recorded with the click; empty for image links. */
|
|
8
8
|
title: string;
|
|
9
|
+
/** Stable anchor id (`data-pw-lid`) recorded with the click; matched before link and text when set. */
|
|
10
|
+
linkId?: string;
|
|
9
11
|
clicks: number;
|
|
10
12
|
};
|
|
11
13
|
/** How the rows of {@link EmailClickMapProps.items} landed on the rendered template. */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import type { InboxPreviewProps } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Renders an Inbox cell the way the SDK of the chosen platform draws it
|
|
5
|
+
* (`platform`: iOS card, Android flat list row, or Web widget card), with the
|
|
6
|
+
* SDK degradation rules: a layout that cannot render falls back to classic and
|
|
7
|
+
* a carousel shows at most five valid slides. Content without a single filled
|
|
8
|
+
* field renders the dashed placeholder of the requested layout.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* <InboxPreview platform="ios" localizedContent={preset.localizedContent} language="en" />
|
|
12
|
+
*/
|
|
13
|
+
export declare function InboxPreview(props: InboxPreviewProps): ReactElement;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Color, ShapeRadius } from '@pushwoosh/kit-constants';
|
|
3
|
+
import { Vertical } from '@pushwoosh/kit-helpers';
|
|
4
|
+
import { InboxAndroidCell } from './components/InboxAndroidCell';
|
|
5
|
+
import { InboxIOSCell } from './components/InboxIOSCell';
|
|
6
|
+
import { InboxWebCell } from './components/InboxWebCell';
|
|
7
|
+
import { INBOX_PREVIEW_MAX_WIDTH, INBOX_PREVIEW_MIN_WIDTH, INBOX_PREVIEW_SKELETON_HEIGHT } from './constants';
|
|
8
|
+
import { getEmptyLayout, getPreviewContent, getPreviewTexts, isEmptyContent, resolveInboxLayout } from './helpers';
|
|
9
|
+
import { Lang } from './polyglot';
|
|
10
|
+
import { RichMessageViewer } from '../RichMessageEditor';
|
|
11
|
+
import { LIGHT_ISLAND_PROPS } from '../shared/lightIsland';
|
|
12
|
+
const CELL_BY_PLATFORM = {
|
|
13
|
+
ios: InboxIOSCell,
|
|
14
|
+
android: InboxAndroidCell,
|
|
15
|
+
web: InboxWebCell
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Renders an Inbox cell the way the SDK of the chosen platform draws it
|
|
19
|
+
* (`platform`: iOS card, Android flat list row, or Web widget card), with the
|
|
20
|
+
* SDK degradation rules: a layout that cannot render falls back to classic and
|
|
21
|
+
* a carousel shows at most five valid slides. Content without a single filled
|
|
22
|
+
* field renders the dashed placeholder of the requested layout.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* <InboxPreview platform="ios" localizedContent={preset.localizedContent} language="en" />
|
|
26
|
+
*/
|
|
27
|
+
export function InboxPreview(props) {
|
|
28
|
+
const {
|
|
29
|
+
platform = 'ios',
|
|
30
|
+
content,
|
|
31
|
+
localizedContent,
|
|
32
|
+
language,
|
|
33
|
+
dateLabel = '12:12',
|
|
34
|
+
unread = true,
|
|
35
|
+
isLoading = false,
|
|
36
|
+
emptyTitle = _jsx(Lang, {
|
|
37
|
+
message: "DUMB.INBOX_PREVIEW.EMPTY_TITLE"
|
|
38
|
+
}),
|
|
39
|
+
emptyDescription = _jsx(Lang, {
|
|
40
|
+
message: "DUMB.INBOX_PREVIEW.EMPTY_DESCRIPTION"
|
|
41
|
+
}),
|
|
42
|
+
width = '100%'
|
|
43
|
+
} = props;
|
|
44
|
+
if (isLoading) {
|
|
45
|
+
return _jsx(Vertical, {
|
|
46
|
+
...LIGHT_ISLAND_PROPS,
|
|
47
|
+
"$bgColor": Color.SOFT_LIGHT,
|
|
48
|
+
"$borderRadius": ShapeRadius.SECTION,
|
|
49
|
+
"$maxWidth": INBOX_PREVIEW_MAX_WIDTH,
|
|
50
|
+
"$minHeight": INBOX_PREVIEW_SKELETON_HEIGHT,
|
|
51
|
+
"$minWidth": INBOX_PREVIEW_MIN_WIDTH,
|
|
52
|
+
"$width": width
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const picked = getPreviewContent(content, localizedContent, language) || {};
|
|
56
|
+
const isEmpty = isEmptyContent(picked);
|
|
57
|
+
const layout = isEmpty ? getEmptyLayout(picked) : resolveInboxLayout(picked);
|
|
58
|
+
const texts = getPreviewTexts(picked, layout);
|
|
59
|
+
const Cell = CELL_BY_PLATFORM[platform];
|
|
60
|
+
return _jsx(Vertical, {
|
|
61
|
+
...LIGHT_ISLAND_PROPS,
|
|
62
|
+
"$maxWidth": INBOX_PREVIEW_MAX_WIDTH,
|
|
63
|
+
"$minWidth": INBOX_PREVIEW_MIN_WIDTH,
|
|
64
|
+
"$position": "relative",
|
|
65
|
+
"$width": width,
|
|
66
|
+
children: _jsx(Cell, {
|
|
67
|
+
body: isEmpty ? emptyDescription : texts.body && _jsx(RichMessageViewer, {
|
|
68
|
+
flat: true,
|
|
69
|
+
value: texts.body
|
|
70
|
+
}),
|
|
71
|
+
content: picked,
|
|
72
|
+
dateLabel: dateLabel,
|
|
73
|
+
isEmpty: isEmpty,
|
|
74
|
+
layout: layout,
|
|
75
|
+
title: isEmpty ? emptyTitle : _jsx(RichMessageViewer, {
|
|
76
|
+
flat: true,
|
|
77
|
+
value: texts.title
|
|
78
|
+
}),
|
|
79
|
+
unread: unread
|
|
80
|
+
})
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { MAX_CAROUSEL_SLIDES } from '../constants';
|
|
2
|
+
import { getEmptyLayout, getPreviewContent, getPreviewTexts, getValidSlides, isEmptyContent, resolveInboxLayout } from '../helpers';
|
|
3
|
+
const HERO = 'https://cdn.example.com/hero.jpg';
|
|
4
|
+
const SLIDE = {
|
|
5
|
+
imageUrl: 'https://cdn.example.com/slide.jpg'
|
|
6
|
+
};
|
|
7
|
+
describe('resolveInboxLayout', () => {
|
|
8
|
+
it('renders the requested layout when it can', () => {
|
|
9
|
+
expect(resolveInboxLayout({
|
|
10
|
+
layoutType: 'classic',
|
|
11
|
+
title: 't',
|
|
12
|
+
body: 'b'
|
|
13
|
+
})).toBe('classic');
|
|
14
|
+
expect(resolveInboxLayout({
|
|
15
|
+
layoutType: 'banner',
|
|
16
|
+
bannerUrl: HERO
|
|
17
|
+
})).toBe('banner');
|
|
18
|
+
expect(resolveInboxLayout({
|
|
19
|
+
layoutType: 'captioned',
|
|
20
|
+
title: 't',
|
|
21
|
+
body: 'b',
|
|
22
|
+
bannerUrl: HERO
|
|
23
|
+
})).toBe('captioned');
|
|
24
|
+
expect(resolveInboxLayout({
|
|
25
|
+
layoutType: 'carousel',
|
|
26
|
+
title: 't',
|
|
27
|
+
body: 'b',
|
|
28
|
+
carousel: [SLIDE]
|
|
29
|
+
})).toBe('carousel');
|
|
30
|
+
});
|
|
31
|
+
it('degrades banner and captioned without a hero image into classic', () => {
|
|
32
|
+
expect(resolveInboxLayout({
|
|
33
|
+
layoutType: 'banner'
|
|
34
|
+
})).toBe('classic');
|
|
35
|
+
expect(resolveInboxLayout({
|
|
36
|
+
layoutType: 'captioned',
|
|
37
|
+
title: 't',
|
|
38
|
+
body: 'b'
|
|
39
|
+
})).toBe('classic');
|
|
40
|
+
});
|
|
41
|
+
it('degrades captioned and carousel without title or body into classic', () => {
|
|
42
|
+
expect(resolveInboxLayout({
|
|
43
|
+
layoutType: 'captioned',
|
|
44
|
+
body: 'b',
|
|
45
|
+
bannerUrl: HERO
|
|
46
|
+
})).toBe('classic');
|
|
47
|
+
expect(resolveInboxLayout({
|
|
48
|
+
layoutType: 'captioned',
|
|
49
|
+
title: 't',
|
|
50
|
+
bannerUrl: HERO
|
|
51
|
+
})).toBe('classic');
|
|
52
|
+
expect(resolveInboxLayout({
|
|
53
|
+
layoutType: 'carousel',
|
|
54
|
+
body: 'b',
|
|
55
|
+
carousel: [SLIDE]
|
|
56
|
+
})).toBe('classic');
|
|
57
|
+
expect(resolveInboxLayout({
|
|
58
|
+
layoutType: 'carousel',
|
|
59
|
+
title: 't',
|
|
60
|
+
carousel: [SLIDE]
|
|
61
|
+
})).toBe('classic');
|
|
62
|
+
});
|
|
63
|
+
it('degrades a carousel without valid slides into classic', () => {
|
|
64
|
+
expect(resolveInboxLayout({
|
|
65
|
+
layoutType: 'carousel',
|
|
66
|
+
title: 't',
|
|
67
|
+
body: 'b'
|
|
68
|
+
})).toBe('classic');
|
|
69
|
+
expect(resolveInboxLayout({
|
|
70
|
+
layoutType: 'carousel',
|
|
71
|
+
title: 't',
|
|
72
|
+
body: 'b',
|
|
73
|
+
carousel: [{
|
|
74
|
+
caption: 'no image'
|
|
75
|
+
}]
|
|
76
|
+
})).toBe('classic');
|
|
77
|
+
});
|
|
78
|
+
it('uses the icon as the hero fallback', () => {
|
|
79
|
+
expect(resolveInboxLayout({
|
|
80
|
+
layoutType: 'banner',
|
|
81
|
+
iconUrl: HERO
|
|
82
|
+
})).toBe('banner');
|
|
83
|
+
});
|
|
84
|
+
it('applies the SDK heuristic to unknown or absent layouts', () => {
|
|
85
|
+
expect(resolveInboxLayout({
|
|
86
|
+
bannerUrl: HERO
|
|
87
|
+
})).toBe('banner');
|
|
88
|
+
expect(resolveInboxLayout({
|
|
89
|
+
title: 't',
|
|
90
|
+
body: 'b',
|
|
91
|
+
bannerUrl: HERO
|
|
92
|
+
})).toBe('captioned');
|
|
93
|
+
expect(resolveInboxLayout({
|
|
94
|
+
title: 't',
|
|
95
|
+
body: 'b'
|
|
96
|
+
})).toBe('classic');
|
|
97
|
+
expect(resolveInboxLayout({
|
|
98
|
+
layoutType: 'video',
|
|
99
|
+
title: 't',
|
|
100
|
+
body: 'b'
|
|
101
|
+
})).toBe('classic');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('getValidSlides', () => {
|
|
105
|
+
it('drops slides without an image and keeps at most the SDK limit', () => {
|
|
106
|
+
const slides = [...Array.from({
|
|
107
|
+
length: MAX_CAROUSEL_SLIDES + 2
|
|
108
|
+
}, (_, index) => ({
|
|
109
|
+
imageUrl: `https://cdn.example.com/${index}.jpg`
|
|
110
|
+
})), {
|
|
111
|
+
caption: 'no image'
|
|
112
|
+
}];
|
|
113
|
+
const valid = getValidSlides(slides);
|
|
114
|
+
expect(valid).toHaveLength(MAX_CAROUSEL_SLIDES);
|
|
115
|
+
expect(valid.every(slide => !!slide.imageUrl)).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe('getPreviewContent', () => {
|
|
119
|
+
const en = {
|
|
120
|
+
title: 'en'
|
|
121
|
+
};
|
|
122
|
+
const de = {
|
|
123
|
+
title: 'de'
|
|
124
|
+
};
|
|
125
|
+
it('prefers the explicit content over the localized map', () => {
|
|
126
|
+
expect(getPreviewContent(de, {
|
|
127
|
+
en
|
|
128
|
+
}, 'en')).toBe(de);
|
|
129
|
+
});
|
|
130
|
+
it('picks by language with default/en/first fallbacks', () => {
|
|
131
|
+
expect(getPreviewContent(null, {
|
|
132
|
+
en,
|
|
133
|
+
de
|
|
134
|
+
}, 'de')).toBe(de);
|
|
135
|
+
expect(getPreviewContent(null, {
|
|
136
|
+
en,
|
|
137
|
+
de
|
|
138
|
+
}, 'fr')).toBe(en);
|
|
139
|
+
expect(getPreviewContent(null, {
|
|
140
|
+
de
|
|
141
|
+
}, 'fr')).toBe(de);
|
|
142
|
+
expect(getPreviewContent(null, undefined, 'en')).toBeNull();
|
|
143
|
+
expect(getPreviewContent(null, {}, 'en')).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe('empty state', () => {
|
|
147
|
+
it('detects content with nothing to render', () => {
|
|
148
|
+
expect(isEmptyContent({
|
|
149
|
+
layoutType: 'captioned'
|
|
150
|
+
})).toBe(true);
|
|
151
|
+
expect(isEmptyContent({
|
|
152
|
+
carousel: [{
|
|
153
|
+
caption: 'no image'
|
|
154
|
+
}]
|
|
155
|
+
})).toBe(true);
|
|
156
|
+
expect(isEmptyContent({
|
|
157
|
+
title: 't'
|
|
158
|
+
})).toBe(false);
|
|
159
|
+
expect(isEmptyContent({
|
|
160
|
+
bannerUrl: 'https://cdn.example.com/hero.jpg'
|
|
161
|
+
})).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
it('shows the placeholder of the requested layout, classic for unknown', () => {
|
|
164
|
+
expect(getEmptyLayout({
|
|
165
|
+
layoutType: 'banner'
|
|
166
|
+
})).toBe('banner');
|
|
167
|
+
expect(getEmptyLayout({
|
|
168
|
+
layoutType: 'video'
|
|
169
|
+
})).toBe('classic');
|
|
170
|
+
expect(getEmptyLayout({})).toBe('classic');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe('getPreviewTexts', () => {
|
|
174
|
+
it('moves the body into the title slot for classic without a title', () => {
|
|
175
|
+
expect(getPreviewTexts({
|
|
176
|
+
body: 'b'
|
|
177
|
+
}, 'classic')).toEqual({
|
|
178
|
+
title: 'b',
|
|
179
|
+
body: ''
|
|
180
|
+
});
|
|
181
|
+
expect(getPreviewTexts({
|
|
182
|
+
title: 't',
|
|
183
|
+
body: 'b'
|
|
184
|
+
}, 'classic')).toEqual({
|
|
185
|
+
title: 't',
|
|
186
|
+
body: 'b'
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
it('keeps the fields as-is for media layouts', () => {
|
|
190
|
+
expect(getPreviewTexts({
|
|
191
|
+
body: 'b'
|
|
192
|
+
}, 'captioned')).toEqual({
|
|
193
|
+
title: '',
|
|
194
|
+
body: 'b'
|
|
195
|
+
});
|
|
196
|
+
expect(getPreviewTexts({}, 'classic')).toEqual({
|
|
197
|
+
title: '',
|
|
198
|
+
body: ''
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Color } from '@pushwoosh/kit-constants';
|
|
3
|
+
import { FlexContainer, Vertical } from '@pushwoosh/kit-helpers';
|
|
4
|
+
import { Text } from '@pushwoosh/kit-typography';
|
|
5
|
+
import { ClampText } from '../../styles';
|
|
6
|
+
import { InboxCellAvatar } from '../InboxCellAvatar';
|
|
7
|
+
import { InboxCellMedia } from '../InboxCellMedia';
|
|
8
|
+
/** Android inbox cell: a flat list row with the time trailing the title. */
|
|
9
|
+
export function InboxAndroidCell(props) {
|
|
10
|
+
const {
|
|
11
|
+
content,
|
|
12
|
+
layout,
|
|
13
|
+
isEmpty,
|
|
14
|
+
title,
|
|
15
|
+
body,
|
|
16
|
+
dateLabel
|
|
17
|
+
} = props;
|
|
18
|
+
const texts = _jsxs(Vertical, {
|
|
19
|
+
"$flex": 1,
|
|
20
|
+
"$gap": 8,
|
|
21
|
+
"$minWidth": 0,
|
|
22
|
+
children: [_jsxs(Text, {
|
|
23
|
+
"$color": Color.MAIN,
|
|
24
|
+
"$ellipsis": true,
|
|
25
|
+
"$variant": "footnote",
|
|
26
|
+
children: [title, ' ', _jsx(Text, {
|
|
27
|
+
"$color": Color.LOCKED,
|
|
28
|
+
"$variant": "footnote",
|
|
29
|
+
children: dateLabel
|
|
30
|
+
})]
|
|
31
|
+
}), !!body && _jsx(ClampText, {
|
|
32
|
+
"$clamp": 3,
|
|
33
|
+
"$color": Color.MAIN,
|
|
34
|
+
"$variant": "footnote",
|
|
35
|
+
children: body
|
|
36
|
+
})]
|
|
37
|
+
});
|
|
38
|
+
if (layout === 'classic') {
|
|
39
|
+
return _jsx(Vertical, {
|
|
40
|
+
"$bgColor": Color.CLEAR,
|
|
41
|
+
"$border": {
|
|
42
|
+
bottom: `1px solid ${Color.DIVIDER}`
|
|
43
|
+
},
|
|
44
|
+
"$gap": 16,
|
|
45
|
+
"$padding": 16,
|
|
46
|
+
children: _jsxs(FlexContainer, {
|
|
47
|
+
"$alignItems": "start",
|
|
48
|
+
"$gap": 12,
|
|
49
|
+
children: [_jsx(InboxCellAvatar, {
|
|
50
|
+
content: content,
|
|
51
|
+
isEmpty: isEmpty,
|
|
52
|
+
platform: "android"
|
|
53
|
+
}), texts]
|
|
54
|
+
})
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return _jsxs(Vertical, {
|
|
58
|
+
"$bgColor": Color.CLEAR,
|
|
59
|
+
"$border": {
|
|
60
|
+
bottom: `1px solid ${Color.DIVIDER}`
|
|
61
|
+
},
|
|
62
|
+
"$gap": 16,
|
|
63
|
+
"$padding": 16,
|
|
64
|
+
children: [_jsx(InboxCellMedia, {
|
|
65
|
+
content: content,
|
|
66
|
+
isEmpty: isEmpty,
|
|
67
|
+
layout: layout
|
|
68
|
+
}), layout !== 'banner' && texts]
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InboxAndroidCell } from './InboxAndroidCell';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InboxAndroidCell } from './InboxAndroidCell';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import type { InboxCellAvatarProps } from './types';
|
|
3
|
+
/** Inbox icon of a cell: the uploaded image, the colored initial the SDK falls back to, or the placeholder. */
|
|
4
|
+
export declare function InboxCellAvatar(props: InboxCellAvatarProps): ReactElement;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Color } from '@pushwoosh/kit-constants';
|
|
3
|
+
import { Horizontal } from '@pushwoosh/kit-helpers';
|
|
4
|
+
import { Text } from '@pushwoosh/kit-typography';
|
|
5
|
+
import { AvatarImage } from './styles';
|
|
6
|
+
import { INBOX_AVATAR } from '../../constants';
|
|
7
|
+
import { getIconInitial } from '../../helpers';
|
|
8
|
+
/** Inbox icon of a cell: the uploaded image, the colored initial the SDK falls back to, or the placeholder. */
|
|
9
|
+
export function InboxCellAvatar(props) {
|
|
10
|
+
const {
|
|
11
|
+
content,
|
|
12
|
+
isEmpty,
|
|
13
|
+
platform
|
|
14
|
+
} = props;
|
|
15
|
+
const {
|
|
16
|
+
size,
|
|
17
|
+
radius,
|
|
18
|
+
initialVariant
|
|
19
|
+
} = INBOX_AVATAR[platform];
|
|
20
|
+
if (isEmpty) {
|
|
21
|
+
return _jsx(Horizontal, {
|
|
22
|
+
"$bgColor": Color.BRIGHT_LIGHT_HOVER,
|
|
23
|
+
"$border": `1px dashed ${Color.BRIGHT}`,
|
|
24
|
+
"$borderRadius": radius,
|
|
25
|
+
"$height": size,
|
|
26
|
+
"$shrink": 0,
|
|
27
|
+
"$width": size
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return _jsx(Horizontal, {
|
|
31
|
+
"$alignItems": "center",
|
|
32
|
+
"$bgColor": Color.PHANTOM,
|
|
33
|
+
"$borderRadius": radius,
|
|
34
|
+
"$height": size,
|
|
35
|
+
"$justifyContent": "center",
|
|
36
|
+
"$overflow": "hidden",
|
|
37
|
+
"$shrink": 0,
|
|
38
|
+
"$width": size,
|
|
39
|
+
children: content.iconUrl ? _jsx(AvatarImage, {
|
|
40
|
+
alt: content.title || '',
|
|
41
|
+
src: content.iconUrl
|
|
42
|
+
}) : _jsx(Text, {
|
|
43
|
+
"$color": Color.MAIN,
|
|
44
|
+
"$variant": initialVariant,
|
|
45
|
+
children: getIconInitial(content)
|
|
46
|
+
})
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InboxCellAvatar } from './InboxCellAvatar';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InboxCellAvatar } from './InboxCellAvatar';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const AvatarImage: import("styled-components").StyledComponent<"img", any, {}, never>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { InboxPreviewContent, InboxPreviewPlatform } from '../../types';
|
|
2
|
+
/** Props of {@link InboxCellAvatar}. */
|
|
3
|
+
export interface InboxCellAvatarProps {
|
|
4
|
+
/** Content whose icon is drawn. */
|
|
5
|
+
content: InboxPreviewContent;
|
|
6
|
+
/** Draw the dashed placeholder instead of the icon. */
|
|
7
|
+
isEmpty: boolean;
|
|
8
|
+
/** Platform whose icon geometry is used. */
|
|
9
|
+
platform: InboxPreviewPlatform;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import type { InboxCellCarouselProps } from './types';
|
|
3
|
+
/** Carousel cell: the slides the SDK would keep, switched by a tap on the image or by a pager dot. */
|
|
4
|
+
export declare function InboxCellCarousel(props: InboxCellCarouselProps): ReactElement;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Horizontal } from '@pushwoosh/kit-helpers';
|
|
4
|
+
import { CarouselViewport, PagerDot, SlideCaption } from './styles';
|
|
5
|
+
import { getValidSlides } from '../../helpers';
|
|
6
|
+
import { HeroImage } from '../../styles';
|
|
7
|
+
/** Carousel cell: the slides the SDK would keep, switched by a tap on the image or by a pager dot. */
|
|
8
|
+
export function InboxCellCarousel(props) {
|
|
9
|
+
const {
|
|
10
|
+
content,
|
|
11
|
+
dotsOnImage = false
|
|
12
|
+
} = props;
|
|
13
|
+
const slides = getValidSlides(content.carousel);
|
|
14
|
+
const [index, setIndex] = useState(0);
|
|
15
|
+
const activeIndex = Math.min(index, slides.length - 1);
|
|
16
|
+
const current = slides[activeIndex];
|
|
17
|
+
const dots = slides.length > 1 && slides.map((_slide, slideIndex) => _jsx(PagerDot, {
|
|
18
|
+
"$active": slideIndex === activeIndex,
|
|
19
|
+
onClick: event => {
|
|
20
|
+
event.stopPropagation();
|
|
21
|
+
setIndex(slideIndex);
|
|
22
|
+
},
|
|
23
|
+
type: "button"
|
|
24
|
+
}, slideIndex));
|
|
25
|
+
return _jsxs(_Fragment, {
|
|
26
|
+
children: [_jsxs(CarouselViewport, {
|
|
27
|
+
onClick: () => setIndex((activeIndex + 1) % slides.length),
|
|
28
|
+
children: [_jsx(HeroImage, {
|
|
29
|
+
alt: current.caption || '',
|
|
30
|
+
src: current.imageUrl
|
|
31
|
+
}), !!current.caption && _jsx(SlideCaption, {
|
|
32
|
+
children: current.caption
|
|
33
|
+
}), dotsOnImage && dots && _jsx(Horizontal, {
|
|
34
|
+
"$alignItems": "center",
|
|
35
|
+
"$bottom": 8,
|
|
36
|
+
"$gap": 6,
|
|
37
|
+
"$justifyContent": "center",
|
|
38
|
+
"$left": 0,
|
|
39
|
+
"$position": "absolute",
|
|
40
|
+
"$right": 0,
|
|
41
|
+
children: dots
|
|
42
|
+
})]
|
|
43
|
+
}), !dotsOnImage && dots && _jsx(Horizontal, {
|
|
44
|
+
"$alignItems": "center",
|
|
45
|
+
"$gap": 6,
|
|
46
|
+
"$justifyContent": "center",
|
|
47
|
+
"$padding": {
|
|
48
|
+
top: 2
|
|
49
|
+
},
|
|
50
|
+
children: dots
|
|
51
|
+
})]
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InboxCellCarousel } from './InboxCellCarousel';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InboxCellCarousel } from './InboxCellCarousel';
|