@pushwoosh/websdk-common 6.18.0 → 6.19.0
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/inbox/InboxCellPreview.d.ts +4 -0
- package/inbox/InboxCellPreview.js +497 -0
- package/inbox/InboxWidgetReader.d.ts +27 -0
- package/inbox/InboxWidgetReader.js +228 -0
- package/inbox/constants.d.ts +15 -0
- package/inbox/constants.js +546 -0
- package/inbox/helpers.d.ts +30 -0
- package/inbox/helpers.js +150 -0
- package/inbox/index.d.ts +6 -0
- package/inbox/index.js +4 -0
- package/inbox/types.d.ts +126 -0
- package/inbox/types.js +1 -0
- package/package.json +6 -2
package/inbox/helpers.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { INBOX_KNOWN_LAYOUTS, INBOX_MAX_CAROUSEL_SLIDES, INBOX_MONTHS, INBOX_WIDGET_DEFAULT_APPEARANCE, INBOX_WIDGET_STYLE_PREFIX, INBOX_WIDGET_STYLE_VARIABLES } from './constants';
|
|
2
|
+
const MINUTE = 60 * 1000;
|
|
3
|
+
const HOUR = 60 * MINUTE;
|
|
4
|
+
const DAY = 24 * HOUR;
|
|
5
|
+
const RULE_BREAKERS = /[;{}]/;
|
|
6
|
+
/** Picks the content to render: explicit `content` wins, otherwise the `language` entry of `localizedContent` with default/en/first fallbacks. */
|
|
7
|
+
export function pickInboxLocalizedContent(content, localizedContent, language) {
|
|
8
|
+
if (content) {
|
|
9
|
+
return content;
|
|
10
|
+
}
|
|
11
|
+
if (!localizedContent) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const keys = Object.keys(localizedContent);
|
|
15
|
+
if (keys.length === 0) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
if (language && localizedContent[language]) {
|
|
19
|
+
return localizedContent[language];
|
|
20
|
+
}
|
|
21
|
+
return localizedContent.default || localizedContent.en || localizedContent[keys[0]];
|
|
22
|
+
}
|
|
23
|
+
/** Slides a cell draws: the ones with an image, at most {@link INBOX_MAX_CAROUSEL_SLIDES}. */
|
|
24
|
+
export function getInboxValidSlides(slides) {
|
|
25
|
+
return (Array.isArray(slides) ? slides : []).filter(slide => !!slide && !!slide.imageUrl).slice(0, INBOX_MAX_CAROUSEL_SLIDES);
|
|
26
|
+
}
|
|
27
|
+
/** Hero image of the banner/captioned layouts: the banner, with the icon standing in when none was sent. */
|
|
28
|
+
export function getInboxHeroUrl(content) {
|
|
29
|
+
return content.bannerUrl || content.iconUrl || '';
|
|
30
|
+
}
|
|
31
|
+
/** Resolves the layout a cell draws: the requested one when it can render, the heuristic for unknown ones, classic otherwise. */
|
|
32
|
+
export function resolveInboxLayout(content) {
|
|
33
|
+
const hasTitle = !!content.title;
|
|
34
|
+
const hasBody = !!content.body;
|
|
35
|
+
const hero = getInboxHeroUrl(content);
|
|
36
|
+
let requested = content.layoutType;
|
|
37
|
+
if (!INBOX_KNOWN_LAYOUTS.includes(requested)) {
|
|
38
|
+
if (content.bannerUrl && !hasTitle) {
|
|
39
|
+
requested = 'banner';
|
|
40
|
+
} else if (content.bannerUrl && hasTitle && hasBody) {
|
|
41
|
+
requested = 'captioned';
|
|
42
|
+
} else {
|
|
43
|
+
requested = 'classic';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
switch (requested) {
|
|
47
|
+
case 'banner':
|
|
48
|
+
return hero ? 'banner' : 'classic';
|
|
49
|
+
case 'captioned':
|
|
50
|
+
return hero && hasTitle && hasBody ? 'captioned' : 'classic';
|
|
51
|
+
case 'carousel':
|
|
52
|
+
return hasTitle && hasBody && getInboxValidSlides(content.carousel).length > 0 ? 'carousel' : 'classic';
|
|
53
|
+
default:
|
|
54
|
+
return 'classic';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** True when the content has nothing to render yet, so a preview shows the layout placeholder. */
|
|
58
|
+
export function isInboxContentEmpty(content) {
|
|
59
|
+
return !content.title && !content.body && !content.iconUrl && !content.bannerUrl && getInboxValidSlides(content.carousel).length === 0;
|
|
60
|
+
}
|
|
61
|
+
/** The layout whose placeholder an empty preview shows: the requested one when known, else classic. */
|
|
62
|
+
export function getInboxEmptyLayout(content) {
|
|
63
|
+
const requested = content.layoutType;
|
|
64
|
+
return INBOX_KNOWN_LAYOUTS.includes(requested) ? requested : 'classic';
|
|
65
|
+
}
|
|
66
|
+
/** Title/body pair of the mobile cells: in classic the body stands in for a missing title, as the mobile SDKs draw it. */
|
|
67
|
+
export function getInboxMobileTexts(content, layout) {
|
|
68
|
+
if (layout === 'classic') {
|
|
69
|
+
return {
|
|
70
|
+
title: content.title || content.body || '',
|
|
71
|
+
body: content.title && content.body || ''
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
title: content.title || '',
|
|
76
|
+
body: content.body || ''
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** The colored initial a classic cell shows when there is no icon. */
|
|
80
|
+
export function getInboxIconInitial(content) {
|
|
81
|
+
return (content.title || content.body || '?').trim().charAt(0).toUpperCase() || '?';
|
|
82
|
+
}
|
|
83
|
+
/** Date label of a web widget message: relative within a day of `now`, then `D Mon YYYY, H:mm` in local time. */
|
|
84
|
+
export function formatInboxMessageTime(sendDate, now = Date.now()) {
|
|
85
|
+
const date = new Date(sendDate);
|
|
86
|
+
const gap = now - date.getTime();
|
|
87
|
+
if (gap <= MINUTE) {
|
|
88
|
+
return 'Just now';
|
|
89
|
+
}
|
|
90
|
+
if (gap < HOUR) {
|
|
91
|
+
return `${Math.floor(gap / MINUTE)} minutes ago`;
|
|
92
|
+
}
|
|
93
|
+
if (gap < DAY) {
|
|
94
|
+
return `${Math.floor(gap / HOUR)} hours ago`;
|
|
95
|
+
}
|
|
96
|
+
const minutes = `0${date.getMinutes()}`.slice(-2);
|
|
97
|
+
return `${date.getDate()} ${INBOX_MONTHS[date.getMonth()]} ${date.getFullYear()}, ${date.getHours()}:${minutes}`;
|
|
98
|
+
}
|
|
99
|
+
/** Newest first. */
|
|
100
|
+
export function compareInboxBySendDate(one, two) {
|
|
101
|
+
return new Date(two).getTime() - new Date(one).getTime();
|
|
102
|
+
}
|
|
103
|
+
/** Full appearance from a partial one; the arrow border follows a visible widget border unless set explicitly. */
|
|
104
|
+
export function resolveInboxWidgetAppearance(appearance) {
|
|
105
|
+
const {
|
|
106
|
+
borderColor
|
|
107
|
+
} = appearance || {};
|
|
108
|
+
return {
|
|
109
|
+
...INBOX_WIDGET_DEFAULT_APPEARANCE,
|
|
110
|
+
arrowBorderColor: borderColor && borderColor !== 'transparent' ? borderColor : INBOX_WIDGET_DEFAULT_APPEARANCE.arrowBorderColor,
|
|
111
|
+
...appearance
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** One appearance value as its CSS custom property value; `null` leaves it unset so the stylesheet default applies. */
|
|
115
|
+
export function formatInboxStyleValue(type, value) {
|
|
116
|
+
switch (type) {
|
|
117
|
+
case 'size':
|
|
118
|
+
{
|
|
119
|
+
if (value === null || value === undefined || value === '') {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const size = Number(value);
|
|
123
|
+
return Number.isFinite(size) ? `${size}px` : null;
|
|
124
|
+
}
|
|
125
|
+
case 'number':
|
|
126
|
+
{
|
|
127
|
+
const parsed = parseFloat(String(value));
|
|
128
|
+
return Number.isFinite(parsed) ? String(parsed) : null;
|
|
129
|
+
}
|
|
130
|
+
default:
|
|
131
|
+
{
|
|
132
|
+
const text = String(value ?? '').trim();
|
|
133
|
+
return text && !RULE_BREAKERS.test(text) ? text : null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** The `--pw-inbox-*` custom properties of an appearance, ready for a `style` object or `setProperty`. */
|
|
138
|
+
export function inboxWidgetStyleVariables(appearance) {
|
|
139
|
+
const variables = {};
|
|
140
|
+
INBOX_WIDGET_STYLE_VARIABLES.forEach(({
|
|
141
|
+
name,
|
|
142
|
+
type
|
|
143
|
+
}) => {
|
|
144
|
+
const formatted = formatInboxStyleValue(type, appearance[name]);
|
|
145
|
+
if (formatted !== null) {
|
|
146
|
+
variables[`--${INBOX_WIDGET_STYLE_PREFIX}-${name}`] = formatted;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return variables;
|
|
150
|
+
}
|
package/inbox/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { InboxWidgetReader, InboxWebCell, inboxWidgetPreviewFrame } from './InboxWidgetReader';
|
|
2
|
+
export { InboxCellPreview } from './InboxCellPreview';
|
|
3
|
+
export { pickInboxLocalizedContent, getInboxValidSlides, getInboxHeroUrl, resolveInboxLayout, isInboxContentEmpty, getInboxEmptyLayout, getInboxMobileTexts, getInboxIconInitial, formatInboxMessageTime, compareInboxBySendDate, resolveInboxWidgetAppearance, formatInboxStyleValue, inboxWidgetStyleVariables, } from './helpers';
|
|
4
|
+
export { INBOX_MAX_CAROUSEL_SLIDES, INBOX_KNOWN_LAYOUTS, INBOX_WIDGET_STYLE_PREFIX, INBOX_MONTHS, INBOX_WIDGET_STYLE_VARIABLES, INBOX_WIDGET_DEFAULT_APPEARANCE, INBOX_WIDGET_CSS, } from './constants';
|
|
5
|
+
export type { InboxWebCellProps } from './InboxWidgetReader';
|
|
6
|
+
export type { InboxLayout, InboxSlide, InboxCellContent, InboxWidgetMessage, InboxWidgetPosition, InboxWidgetAppearance, InboxWidgetStyleName, InboxWidgetStyleType, InboxWidgetStyleVariable, InboxWidgetReaderMode, InboxWidgetReaderProps, InboxCellPlatform, InboxCellPreviewProps, } from './types';
|
package/inbox/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { InboxWidgetReader, InboxWebCell, inboxWidgetPreviewFrame } from './InboxWidgetReader';
|
|
2
|
+
export { InboxCellPreview } from './InboxCellPreview';
|
|
3
|
+
export { pickInboxLocalizedContent, getInboxValidSlides, getInboxHeroUrl, resolveInboxLayout, isInboxContentEmpty, getInboxEmptyLayout, getInboxMobileTexts, getInboxIconInitial, formatInboxMessageTime, compareInboxBySendDate, resolveInboxWidgetAppearance, formatInboxStyleValue, inboxWidgetStyleVariables } from './helpers';
|
|
4
|
+
export { INBOX_MAX_CAROUSEL_SLIDES, INBOX_KNOWN_LAYOUTS, INBOX_WIDGET_STYLE_PREFIX, INBOX_MONTHS, INBOX_WIDGET_STYLE_VARIABLES, INBOX_WIDGET_DEFAULT_APPEARANCE, INBOX_WIDGET_CSS } from './constants';
|
package/inbox/types.d.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/** Cell layout of an inbox message. */
|
|
3
|
+
export type InboxLayout = 'classic' | 'captioned' | 'banner' | 'carousel';
|
|
4
|
+
/** One carousel slide of an inbox message. */
|
|
5
|
+
export interface InboxSlide {
|
|
6
|
+
/** Slide image URL; a slide without it is dropped. */
|
|
7
|
+
imageUrl?: string;
|
|
8
|
+
/** Caption overlaid at the bottom of the slide. */
|
|
9
|
+
caption?: string;
|
|
10
|
+
/** Where a tap on the slide goes; the host validates it before navigating. */
|
|
11
|
+
link?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Content of one inbox cell, in the shape both the Control Panel and the WebSDK map their data to. */
|
|
14
|
+
export interface InboxCellContent {
|
|
15
|
+
/** Cell title. */
|
|
16
|
+
title?: string;
|
|
17
|
+
/** Cell text. */
|
|
18
|
+
body?: string;
|
|
19
|
+
/** Small avatar icon URL; without it classic renders a colored initial. */
|
|
20
|
+
iconUrl?: string;
|
|
21
|
+
/** The campaign's own banner for the banner/captioned layouts. */
|
|
22
|
+
bannerUrl?: string;
|
|
23
|
+
/** Requested layout; unknown or absent values fall back to the heuristic of {@link resolveInboxLayout}. */
|
|
24
|
+
layoutType?: string;
|
|
25
|
+
/** Carousel slides; only the first valid ones are rendered. */
|
|
26
|
+
carousel?: InboxSlide[];
|
|
27
|
+
}
|
|
28
|
+
/** One message of the web inbox widget. */
|
|
29
|
+
export interface InboxWidgetMessage extends InboxCellContent {
|
|
30
|
+
/** Message code, rendered as `data-pw-inbox-message-id`. */
|
|
31
|
+
code: string;
|
|
32
|
+
/** Send date as an ISO string (UTC). */
|
|
33
|
+
sendDate: string;
|
|
34
|
+
/** The message was seen in the widget. */
|
|
35
|
+
isRead: boolean;
|
|
36
|
+
/** The message action was performed. */
|
|
37
|
+
isActionPerformed: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** Position of the widget relative to its trigger. */
|
|
40
|
+
export type InboxWidgetPosition = 'left' | 'right' | 'top' | 'bottom';
|
|
41
|
+
/** Appearance of the web inbox widget; every color and size is also a `--pw-inbox-<name>` CSS custom property. */
|
|
42
|
+
export interface InboxWidgetAppearance {
|
|
43
|
+
title: string;
|
|
44
|
+
bgColor: string;
|
|
45
|
+
textColor: string;
|
|
46
|
+
fontFamily: string;
|
|
47
|
+
borderRadius: number;
|
|
48
|
+
borderColor: string;
|
|
49
|
+
badgeBgColor: string;
|
|
50
|
+
badgeTextColor: string;
|
|
51
|
+
widgetWidth: number;
|
|
52
|
+
zIndex: number;
|
|
53
|
+
messageTitleColor: string;
|
|
54
|
+
timeTextColor: string;
|
|
55
|
+
emptyInboxTitle: string;
|
|
56
|
+
emptyInboxText: string;
|
|
57
|
+
emptyInboxIconUrl: string;
|
|
58
|
+
emptyInboxTitleColor: string;
|
|
59
|
+
emptyInboxTextColor: string;
|
|
60
|
+
arrowBorderColor: string;
|
|
61
|
+
/** Keep the pre-3.75 look of the classic cell; the other layouts are unaffected. */
|
|
62
|
+
legacyClassicCell: boolean;
|
|
63
|
+
unreadDotColor: string;
|
|
64
|
+
avatarBgColor: string;
|
|
65
|
+
avatarTextColor: string;
|
|
66
|
+
avatarSize: number;
|
|
67
|
+
mediaRadius: number;
|
|
68
|
+
slideCaptionColor: string;
|
|
69
|
+
slideScrimColor: string;
|
|
70
|
+
pagerDotActiveColor: string;
|
|
71
|
+
pagerDotColor: string;
|
|
72
|
+
}
|
|
73
|
+
/** Appearance keys exposed as `--pw-inbox-<name>` CSS custom properties. */
|
|
74
|
+
export type InboxWidgetStyleName = Exclude<keyof InboxWidgetAppearance, 'title' | 'emptyInboxTitle' | 'emptyInboxText' | 'emptyInboxIconUrl' | 'legacyClassicCell'>;
|
|
75
|
+
/** How an appearance value is formatted into its CSS custom property. */
|
|
76
|
+
export type InboxWidgetStyleType = 'number' | 'color' | 'string' | 'size';
|
|
77
|
+
/** One CSS custom property of the widget. */
|
|
78
|
+
export interface InboxWidgetStyleVariable {
|
|
79
|
+
name: InboxWidgetStyleName;
|
|
80
|
+
type: InboxWidgetStyleType;
|
|
81
|
+
}
|
|
82
|
+
/** `live` renders into the host's `.pw-inbox-widget` element; `preview` draws the widget box itself, inline. */
|
|
83
|
+
export type InboxWidgetReaderMode = 'live' | 'preview';
|
|
84
|
+
/** Props of {@link InboxWidgetReader}. */
|
|
85
|
+
export interface InboxWidgetReaderProps {
|
|
86
|
+
mode: InboxWidgetReaderMode;
|
|
87
|
+
/** Messages in display order. */
|
|
88
|
+
messages: InboxWidgetMessage[];
|
|
89
|
+
/** Appearance; missing keys take {@link INBOX_WIDGET_DEFAULT_APPEARANCE}. */
|
|
90
|
+
appearance?: Partial<InboxWidgetAppearance>;
|
|
91
|
+
/** Reference time for the relative dates, ms since epoch (default: now). */
|
|
92
|
+
now?: number;
|
|
93
|
+
/** A tap on a message outside its remove icon, pager and slide link. */
|
|
94
|
+
onMessageClick?: (code: string) => void;
|
|
95
|
+
/** A tap on the remove icon of a message. */
|
|
96
|
+
onMessageRemove?: (code: string) => void;
|
|
97
|
+
/** A tap on a slide that carries a link. */
|
|
98
|
+
onSlideLinkClick?: (code: string, link: string) => void;
|
|
99
|
+
}
|
|
100
|
+
/** Platform skin of {@link InboxCellPreview}. */
|
|
101
|
+
export type InboxCellPlatform = 'ios' | 'android' | 'web';
|
|
102
|
+
/** Props of {@link InboxCellPreview}. */
|
|
103
|
+
export interface InboxCellPreviewProps {
|
|
104
|
+
/** Platform skin (default `ios`). */
|
|
105
|
+
platform?: InboxCellPlatform;
|
|
106
|
+
/** Content to draw; empty content draws the placeholder of the requested layout. */
|
|
107
|
+
content?: InboxCellContent | null;
|
|
108
|
+
/** Date label of the mobile cells (default `12:12`); the web cell derives it from `sendDate`. */
|
|
109
|
+
dateLabel?: string;
|
|
110
|
+
/** Send date of the web cell (default: now). */
|
|
111
|
+
sendDate?: string;
|
|
112
|
+
/** Draw the unread dot (default `true`). */
|
|
113
|
+
unread?: boolean;
|
|
114
|
+
/** Show the loading placeholder. */
|
|
115
|
+
isLoading?: boolean;
|
|
116
|
+
/** Title of the empty placeholder. */
|
|
117
|
+
emptyTitle?: ReactNode;
|
|
118
|
+
/** Description of the empty placeholder. */
|
|
119
|
+
emptyDescription?: ReactNode;
|
|
120
|
+
/** Renders the title/body of the mobile cells (e.g. a rich-text viewer); plain text by default. */
|
|
121
|
+
renderText?: (text: string) => ReactNode;
|
|
122
|
+
/** Appearance of the web cell. */
|
|
123
|
+
appearance?: Partial<InboxWidgetAppearance>;
|
|
124
|
+
/** CSS width of the preview (default `100%`). */
|
|
125
|
+
width?: string | number;
|
|
126
|
+
}
|
package/inbox/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushwoosh/websdk-common",
|
|
3
|
-
"version": "6.
|
|
4
|
-
"description": "Code shared by the Pushwoosh WebSDK and the Control Panel, one entry point per module (web-popups: web popup model and renderer; subscription-widget: push subscription widget model and renderer)",
|
|
3
|
+
"version": "6.19.0",
|
|
4
|
+
"description": "Code shared by the Pushwoosh WebSDK and the Control Panel, one entry point per module (web-popups: web popup model and renderer; subscription-widget: push subscription widget model and renderer; inbox: web inbox widget and inbox cell previews)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"types": "./subscription-widget/index.d.ts",
|
|
19
19
|
"default": "./subscription-widget/index.js"
|
|
20
20
|
},
|
|
21
|
+
"./inbox": {
|
|
22
|
+
"types": "./inbox/index.d.ts",
|
|
23
|
+
"default": "./inbox/index.js"
|
|
24
|
+
},
|
|
21
25
|
"./package.json": "./package.json"
|
|
22
26
|
},
|
|
23
27
|
"sideEffects": false,
|