@pushwoosh/smart-components-kit 1.3.5 → 1.3.7
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/MediaStorage/MediaStorage.js +16 -6
- package/MediaStorage/MediaStoragePaginated.js +6 -2
- package/MediaStorage/styled.d.ts +1 -0
- package/MediaStorage/styled.js +21 -17
- package/MediaStorage/types.d.ts +18 -2
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/mmsAttachments/index.d.ts +1 -0
- package/mmsAttachments/index.js +1 -0
- package/mmsAttachments/rules.d.ts +35 -0
- package/mmsAttachments/rules.js +46 -0
- package/package.json +2 -2
|
@@ -9,7 +9,7 @@ import { CreateCropDrawer } from './CreateCropDrawer';
|
|
|
9
9
|
import { useDeleteMediaFile } from './hooks';
|
|
10
10
|
import { formatBytes, getCropCaption, getThumbUrl } from './mediaFile.utils';
|
|
11
11
|
import { useLang } from './polyglot';
|
|
12
|
-
import { CropBadge, Dropzone, EmptyState, Grid, HiddenInput, Tile, TileActions, TileImage, UsageLine } from './styled';
|
|
12
|
+
import { CropBadge, Dropzone, EmptyState, Grid, HiddenInput, Tile, TileActions, TileImage, UsageLine, WarningBadge } from './styled';
|
|
13
13
|
const UPLOAD_PATH = '/api/media/upload';
|
|
14
14
|
const ALLOWED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
|
15
15
|
const isAllowedImage = file => {
|
|
@@ -26,7 +26,9 @@ export const MediaStorage = ({
|
|
|
26
26
|
page,
|
|
27
27
|
perPage,
|
|
28
28
|
onPageChange,
|
|
29
|
-
onSelect
|
|
29
|
+
onSelect,
|
|
30
|
+
extensions,
|
|
31
|
+
fileWarning
|
|
30
32
|
}) => {
|
|
31
33
|
const lang = useLang();
|
|
32
34
|
const {
|
|
@@ -56,18 +58,21 @@ export const MediaStorage = ({
|
|
|
56
58
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
57
59
|
const [createCropFor, setCreateCropFor] = useState(null);
|
|
58
60
|
const [pendingDelete, setPendingDelete] = useState(null);
|
|
61
|
+
// Format filtering belongs to the query, not the response: `total` and the page count come
|
|
62
|
+
// from the filtered set, so a client-side filter would paginate over files it then hides.
|
|
59
63
|
const listAsync = useAsync(async () => {
|
|
60
64
|
const response = await rpcGateway.mediaFiles.List({
|
|
61
65
|
application,
|
|
62
66
|
page,
|
|
63
|
-
perPage
|
|
67
|
+
perPage,
|
|
68
|
+
extensions
|
|
64
69
|
});
|
|
65
70
|
// protobuf JSON omits empty `repeated` fields, so `files` can be undefined.
|
|
66
71
|
return {
|
|
67
72
|
files: response.files ?? [],
|
|
68
73
|
total: response.total
|
|
69
74
|
};
|
|
70
|
-
}, [rpcGateway, application, page, perPage, reloadToken]);
|
|
75
|
+
}, [rpcGateway, application, page, perPage, extensions, reloadToken]);
|
|
71
76
|
const usageAsync = useAsync(() => rpcGateway.mediaFiles.GetUsage({
|
|
72
77
|
application
|
|
73
78
|
}), [rpcGateway, application, reloadToken, usageToken]);
|
|
@@ -103,7 +108,8 @@ export const MediaStorage = ({
|
|
|
103
108
|
try {
|
|
104
109
|
const response = await rpcGateway.mediaFiles.List({
|
|
105
110
|
application,
|
|
106
|
-
parentUuid: file.uuid
|
|
111
|
+
parentUuid: file.uuid,
|
|
112
|
+
extensions
|
|
107
113
|
});
|
|
108
114
|
if (cancelled) return;
|
|
109
115
|
setCropsByUuid(prev => ({
|
|
@@ -118,7 +124,7 @@ export const MediaStorage = ({
|
|
|
118
124
|
return () => {
|
|
119
125
|
cancelled = true;
|
|
120
126
|
};
|
|
121
|
-
}, [listAsync.value, rpcGateway, application]);
|
|
127
|
+
}, [listAsync.value, rpcGateway, application, extensions]);
|
|
122
128
|
const uploadFiles = async selected => {
|
|
123
129
|
if (selected.length === 0) return;
|
|
124
130
|
const images = selected.filter(isAllowedImage);
|
|
@@ -246,6 +252,7 @@ export const MediaStorage = ({
|
|
|
246
252
|
}), !loading && files.length > 0 && _jsx(Grid, {
|
|
247
253
|
children: files.map(file => {
|
|
248
254
|
const crops = cropsByUuid[file.uuid] ?? [];
|
|
255
|
+
const warning = fileWarning?.(file);
|
|
249
256
|
return _jsxs(Tile, {
|
|
250
257
|
children: [_jsx(TileImage, {
|
|
251
258
|
src: getThumbUrl(file),
|
|
@@ -255,6 +262,9 @@ export const MediaStorage = ({
|
|
|
255
262
|
message: 'SMART.MEDIA_STORAGE.CROPS_BADGE',
|
|
256
263
|
count: crops.length
|
|
257
264
|
})
|
|
265
|
+
}), warning && _jsx(WarningBadge, {
|
|
266
|
+
title: warning,
|
|
267
|
+
children: warning
|
|
258
268
|
}), _jsxs(TileActions, {
|
|
259
269
|
children: [crops.length > 0 ? _jsx(DropdownMenuButton, {
|
|
260
270
|
color: "secondary",
|
|
@@ -11,7 +11,9 @@ export const MediaStoragePaginated = ({
|
|
|
11
11
|
application,
|
|
12
12
|
initialPage = 0,
|
|
13
13
|
perPage = DEFAULT_PER_PAGE,
|
|
14
|
-
onSelect
|
|
14
|
+
onSelect,
|
|
15
|
+
extensions,
|
|
16
|
+
fileWarning
|
|
15
17
|
}) => {
|
|
16
18
|
const [page, setPage] = useState(initialPage);
|
|
17
19
|
return _jsx(MediaStorage, {
|
|
@@ -19,6 +21,8 @@ export const MediaStoragePaginated = ({
|
|
|
19
21
|
page: page,
|
|
20
22
|
perPage: perPage,
|
|
21
23
|
onPageChange: setPage,
|
|
22
|
-
onSelect: onSelect
|
|
24
|
+
onSelect: onSelect,
|
|
25
|
+
extensions: extensions,
|
|
26
|
+
fileWarning: fileWarning
|
|
23
27
|
});
|
|
24
28
|
};
|
package/MediaStorage/styled.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export declare const Grid: import("styled-components").StyledComponent<"div", an
|
|
|
2
2
|
export declare const Tile: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
3
3
|
export declare const TileImage: import("styled-components").StyledComponent<"img", any, {}, never>;
|
|
4
4
|
export declare const CropBadge: import("styled-components").StyledComponent<"span", any, {}, never>;
|
|
5
|
+
export declare const WarningBadge: import("styled-components").StyledComponent<"span", any, {}, never>;
|
|
5
6
|
export declare const TileActions: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
6
7
|
export declare const EmptyState: import("styled-components").StyledComponent<"div", any, {}, never>;
|
|
7
8
|
export declare const Dropzone: import("styled-components").StyledComponent<"button", any, {
|
package/MediaStorage/styled.js
CHANGED
|
@@ -17,17 +17,21 @@ export const CropBadge = styled.span.withConfig({
|
|
|
17
17
|
displayName: "CropBadge",
|
|
18
18
|
componentId: "sc-14317rj-3"
|
|
19
19
|
})(["position:absolute;top:6px;right:6px;font-size:10px;font-weight:", ";line-height:1;color:", ";background:", ";border-radius:20px;padding:3px 7px;pointer-events:none;"], FontWeight.BOLD, Color.CLEAR, Color.PRIMARY);
|
|
20
|
+
export const WarningBadge = styled.span.withConfig({
|
|
21
|
+
displayName: "WarningBadge",
|
|
22
|
+
componentId: "sc-14317rj-4"
|
|
23
|
+
})(["position:absolute;bottom:0;left:0;right:0;font-size:10px;line-height:1.2;text-align:center;color:", ";background:", ";padding:3px 6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:none;"], Color.CLEAR, Color.WARNING);
|
|
20
24
|
export const TileActions = styled.div.withConfig({
|
|
21
25
|
displayName: "TileActions",
|
|
22
|
-
componentId: "sc-14317rj-
|
|
26
|
+
componentId: "sc-14317rj-5"
|
|
23
27
|
})(["position:absolute;top:6px;left:6px;display:flex;gap:6px;padding:4px;border-radius:8px;background:rgba(0,0,0,0.45);backdrop-filter:blur(2px);opacity:0;transition:opacity 0.15s;", ":hover &{opacity:1;}@media (hover:none){opacity:1;}"], Tile);
|
|
24
28
|
export const EmptyState = styled.div.withConfig({
|
|
25
29
|
displayName: "EmptyState",
|
|
26
|
-
componentId: "sc-14317rj-
|
|
30
|
+
componentId: "sc-14317rj-6"
|
|
27
31
|
})(["text-align:center;padding:48px 16px;color:", ";"], Color.LOCKED);
|
|
28
32
|
export const Dropzone = styled.button.withConfig({
|
|
29
33
|
displayName: "Dropzone",
|
|
30
|
-
componentId: "sc-14317rj-
|
|
34
|
+
componentId: "sc-14317rj-7"
|
|
31
35
|
})(["width:100%;min-height:96px;margin-bottom:16px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:16px;font:inherit;font-size:13px;color:", ";cursor:pointer;border:2px dashed ", ";border-radius:8px;background:", ";transition:border-color 0.15s,background 0.15s;&:hover{border-color:", ";}&:disabled{cursor:default;opacity:0.7;}"], Color.LOCKED, ({
|
|
32
36
|
$dragOver
|
|
33
37
|
}) => $dragOver ? Color.PRIMARY : Color.DIVIDER, ({
|
|
@@ -35,46 +39,46 @@ export const Dropzone = styled.button.withConfig({
|
|
|
35
39
|
}) => $dragOver ? Color.SOFT_LIGHT : Color.CLEAR, Color.PRIMARY);
|
|
36
40
|
export const HiddenInput = styled.input.withConfig({
|
|
37
41
|
displayName: "HiddenInput",
|
|
38
|
-
componentId: "sc-14317rj-
|
|
42
|
+
componentId: "sc-14317rj-8"
|
|
39
43
|
})(["display:none;"]);
|
|
40
44
|
export const UsageLine = styled.div.withConfig({
|
|
41
45
|
displayName: "UsageLine",
|
|
42
|
-
componentId: "sc-14317rj-
|
|
46
|
+
componentId: "sc-14317rj-9"
|
|
43
47
|
})(["margin-left:auto;font-size:", ";color:", ";"], FontSize.SMALL, Color.LOCKED);
|
|
44
48
|
/* ---------- CreateCropDrawer ---------- */
|
|
45
49
|
export const PreviewStage = styled.div.withConfig({
|
|
46
50
|
displayName: "PreviewStage",
|
|
47
|
-
componentId: "sc-14317rj-
|
|
51
|
+
componentId: "sc-14317rj-10"
|
|
48
52
|
})(["display:flex;align-items:center;justify-content:center;min-height:200px;max-height:360px;padding:12px;border:1px solid ", ";border-radius:6px;overflow:hidden;background:", ";"], Color.DIVIDER, Color.SOFT_LIGHT);
|
|
49
53
|
export const PreviewFrame = styled.div.withConfig({
|
|
50
54
|
displayName: "PreviewFrame",
|
|
51
|
-
componentId: "sc-14317rj-
|
|
55
|
+
componentId: "sc-14317rj-11"
|
|
52
56
|
})(["aspect-ratio:", ";max-width:100%;max-height:336px;overflow:hidden;border-radius:4px;"], ({
|
|
53
57
|
$ratio
|
|
54
58
|
}) => $ratio);
|
|
55
59
|
export const PreviewImage = styled.img.withConfig({
|
|
56
60
|
displayName: "PreviewImage",
|
|
57
|
-
componentId: "sc-14317rj-
|
|
61
|
+
componentId: "sc-14317rj-12"
|
|
58
62
|
})(["width:100%;height:100%;object-fit:cover;display:block;"]);
|
|
59
63
|
export const FieldLabel = styled.div.withConfig({
|
|
60
64
|
displayName: "FieldLabel",
|
|
61
|
-
componentId: "sc-14317rj-
|
|
65
|
+
componentId: "sc-14317rj-13"
|
|
62
66
|
})(["font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.4px;color:", ";margin-bottom:6px;"], Color.LOCKED);
|
|
63
67
|
export const SizeInput = styled(NativeInput).withConfig({
|
|
64
68
|
displayName: "SizeInput",
|
|
65
|
-
componentId: "sc-14317rj-
|
|
69
|
+
componentId: "sc-14317rj-14"
|
|
66
70
|
})(["&[readonly]{background:", ";color:", ";}"], Color.SOFT_LIGHT, Color.LOCKED);
|
|
67
71
|
export const Chips = styled.div.withConfig({
|
|
68
72
|
displayName: "Chips",
|
|
69
|
-
componentId: "sc-14317rj-
|
|
73
|
+
componentId: "sc-14317rj-15"
|
|
70
74
|
})(["display:flex;flex-wrap:wrap;gap:7px;"]);
|
|
71
75
|
export const Segment = styled.div.withConfig({
|
|
72
76
|
displayName: "Segment",
|
|
73
|
-
componentId: "sc-14317rj-
|
|
77
|
+
componentId: "sc-14317rj-16"
|
|
74
78
|
})(["display:inline-flex;border:1px solid ", ";border-radius:6px;overflow:hidden;"], Color.DIVIDER);
|
|
75
79
|
export const SegmentItem = styled.button.withConfig({
|
|
76
80
|
displayName: "SegmentItem",
|
|
77
|
-
componentId: "sc-14317rj-
|
|
81
|
+
componentId: "sc-14317rj-17"
|
|
78
82
|
})(["font:inherit;font-size:13px;cursor:pointer;padding:7px 14px;border:none;background:", ";color:", ";& + &{border-left:1px solid ", ";}"], ({
|
|
79
83
|
$active
|
|
80
84
|
}) => $active ? Color.PRIMARY : Color.CLEAR, ({
|
|
@@ -82,17 +86,17 @@ export const SegmentItem = styled.button.withConfig({
|
|
|
82
86
|
}) => $active ? Color.CLEAR : Color.MAIN, Color.DIVIDER);
|
|
83
87
|
export const CropStrip = styled.div.withConfig({
|
|
84
88
|
displayName: "CropStrip",
|
|
85
|
-
componentId: "sc-14317rj-
|
|
89
|
+
componentId: "sc-14317rj-18"
|
|
86
90
|
})(["display:flex;gap:10px;flex-wrap:wrap;align-items:flex-start;"]);
|
|
87
91
|
export const CropThumb = styled.div.withConfig({
|
|
88
92
|
displayName: "CropThumb",
|
|
89
|
-
componentId: "sc-14317rj-
|
|
93
|
+
componentId: "sc-14317rj-19"
|
|
90
94
|
})(["position:relative;width:88px;border:1px solid ", ";border-radius:6px;overflow:hidden;background:", ";"], Color.DIVIDER, Color.SOFT_LIGHT);
|
|
91
95
|
export const CropThumbDelete = styled.div.withConfig({
|
|
92
96
|
displayName: "CropThumbDelete",
|
|
93
|
-
componentId: "sc-14317rj-
|
|
97
|
+
componentId: "sc-14317rj-20"
|
|
94
98
|
})(["position:absolute;top:4px;right:4px;opacity:0;transition:opacity 0.15s;", ":hover &{opacity:1;}@media (hover:none){opacity:1;}"], CropThumb);
|
|
95
99
|
export const CropThumbCaption = styled.div.withConfig({
|
|
96
100
|
displayName: "CropThumbCaption",
|
|
97
|
-
componentId: "sc-14317rj-
|
|
101
|
+
componentId: "sc-14317rj-21"
|
|
98
102
|
})(["font-size:10px;text-align:center;padding:3px 2px;color:", ";"], Color.LOCKED);
|
package/MediaStorage/types.d.ts
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import type { MediaFile } from '@pushwoosh/rpc-v2-http-api-data/media_files';
|
|
2
|
-
|
|
2
|
+
/** Per-channel options both gallery entry points accept; `MediaStoragePaginated` forwards them as-is. */
|
|
3
|
+
type MediaFileChannelOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* Shows only files whose stored path ends with one of these extensions (`"jpg"` or `".jpg"`,
|
|
6
|
+
* case-insensitive); omit to show the whole library. The list goes into the `List` request,
|
|
7
|
+
* so `total` and the page count match what is rendered — filtering the response instead
|
|
8
|
+
* would let a page collapse to zero tiles while later pages still hold matches.
|
|
9
|
+
*/
|
|
10
|
+
extensions?: string[];
|
|
11
|
+
/**
|
|
12
|
+
* Warning to show on a file's tile, or undefined for none. The text belongs to the caller:
|
|
13
|
+
* the kit has no opinion on why a file is risky for a particular channel.
|
|
14
|
+
*/
|
|
15
|
+
fileWarning?: (file: MediaFile) => string | undefined;
|
|
16
|
+
};
|
|
17
|
+
export type MediaStorageProps = MediaFileChannelOptions & {
|
|
3
18
|
/** Application code whose media files are listed. */
|
|
4
19
|
application: string;
|
|
5
20
|
/** Current page (zero-based). Controlled by the parent. */
|
|
@@ -12,7 +27,7 @@ export type MediaStorageProps = {
|
|
|
12
27
|
* asset, `uuid` its gallery id. */
|
|
13
28
|
onSelect: (file: MediaFile) => void;
|
|
14
29
|
};
|
|
15
|
-
export type MediaStoragePaginatedProps = {
|
|
30
|
+
export type MediaStoragePaginatedProps = MediaFileChannelOptions & {
|
|
16
31
|
/** Application code whose media files are listed. */
|
|
17
32
|
application: string;
|
|
18
33
|
/** Initial page to load (zero-based). Defaults to 0. */
|
|
@@ -23,3 +38,4 @@ export type MediaStoragePaginatedProps = {
|
|
|
23
38
|
* asset, `uuid` its gallery id. */
|
|
24
39
|
onSelect: (file: MediaFile) => void;
|
|
25
40
|
};
|
|
41
|
+
export {};
|
package/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from './ExportCSV';
|
|
|
8
8
|
export * from './PlatformsConfigurationDrawer';
|
|
9
9
|
export * from './LanguagesSelector';
|
|
10
10
|
export * from './MediaStorage';
|
|
11
|
+
export * from './mmsAttachments';
|
|
11
12
|
export * from './ResendModal';
|
|
12
13
|
export * from './RichMessageEditorSmart';
|
|
13
14
|
export * from './Table';
|
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export * from './ExportCSV';
|
|
|
8
8
|
export * from './PlatformsConfigurationDrawer';
|
|
9
9
|
export * from './LanguagesSelector';
|
|
10
10
|
export * from './MediaStorage';
|
|
11
|
+
export * from './mmsAttachments';
|
|
11
12
|
export * from './ResendModal';
|
|
12
13
|
export * from './RichMessageEditorSmart';
|
|
13
14
|
export * from './Table';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MMS_ALLOWED_EXTENSIONS, MMS_MAX_ATTACHMENTS, MMS_MAX_FILE_SIZE_BYTES, MMS_SUBJECT_MAX_ASCII_LENGTH, MMS_SUBJECT_MAX_UNICODE_LENGTH, getMmsSubjectLength, getMmsSubjectMaxLength, isMmsAttachmentOversized, isMmsAttachmentUrl, isMmsMessageAtValid, isMmsSubjectTooLong, } from './rules';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MMS_ALLOWED_EXTENSIONS, MMS_MAX_ATTACHMENTS, MMS_MAX_FILE_SIZE_BYTES, MMS_SUBJECT_MAX_ASCII_LENGTH, MMS_SUBJECT_MAX_UNICODE_LENGTH, getMmsSubjectLength, getMmsSubjectMaxLength, isMmsAttachmentOversized, isMmsAttachmentUrl, isMmsMessageAtValid, isMmsSubjectTooLong } from './rules';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { MediaFile } from '@pushwoosh/rpc-v2-http-api-data/media_files';
|
|
2
|
+
/**
|
|
3
|
+
* Limits mirrored from messaging-api validation (`internal/api/validation/content-sms.go`).
|
|
4
|
+
* Breaking any of them is a hard 2073/2074 error on send, so the form must not let it through.
|
|
5
|
+
*/
|
|
6
|
+
export declare const MMS_MAX_ATTACHMENTS = 3;
|
|
7
|
+
export declare const MMS_SUBJECT_MAX_ASCII_LENGTH = 40;
|
|
8
|
+
export declare const MMS_SUBJECT_MAX_UNICODE_LENGTH = 13;
|
|
9
|
+
export declare const MMS_ALLOWED_EXTENSIONS: string[];
|
|
10
|
+
/**
|
|
11
|
+
* The provider rejects anything heavier, so an oversized attachment fails the whole send.
|
|
12
|
+
* Media-library sizes are not always known, so the forms warn on it instead of blocking.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MMS_MAX_FILE_SIZE_BYTES: number;
|
|
15
|
+
/**
|
|
16
|
+
* The provider downloads the attachment itself, so it has to be a public https url whose
|
|
17
|
+
* *path* ends with a supported image extension. Checking the path (not the whole url) matches
|
|
18
|
+
* the backend and keeps query strings — signatures, cache busters — from passing as extensions.
|
|
19
|
+
*
|
|
20
|
+
* PNG, JPEG, WebP and the legacy media-library files stored without an extension drop out
|
|
21
|
+
* here: all of them are fetchable, none is decoded by the provider.
|
|
22
|
+
*/
|
|
23
|
+
export declare const isMmsAttachmentUrl: (url: string) => boolean;
|
|
24
|
+
/** Whether the file is over the provider's size limit and will be rejected on send. */
|
|
25
|
+
export declare const isMmsAttachmentOversized: (file: MediaFile) => boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Subject length budget: the provider counts non-ASCII subjects against a much shorter
|
|
28
|
+
* limit, so a single non-ASCII character drops the whole subject to 13.
|
|
29
|
+
*/
|
|
30
|
+
export declare const getMmsSubjectMaxLength: (subject: string) => number;
|
|
31
|
+
/** Subject length in characters, counting astral symbols (emoji) as one. */
|
|
32
|
+
export declare const getMmsSubjectLength: (subject: string) => number;
|
|
33
|
+
export declare const isMmsSubjectTooLong: (subject: string) => boolean;
|
|
34
|
+
/** Whether message_at points at one of the attachments, as the backend requires. */
|
|
35
|
+
export declare const isMmsMessageAtValid: (messageAt: number, attachmentsCount: number) => boolean;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Limits mirrored from messaging-api validation (`internal/api/validation/content-sms.go`).
|
|
3
|
+
* Breaking any of them is a hard 2073/2074 error on send, so the form must not let it through.
|
|
4
|
+
*/
|
|
5
|
+
export const MMS_MAX_ATTACHMENTS = 3;
|
|
6
|
+
export const MMS_SUBJECT_MAX_ASCII_LENGTH = 40;
|
|
7
|
+
export const MMS_SUBJECT_MAX_UNICODE_LENGTH = 13;
|
|
8
|
+
export const MMS_ALLOWED_EXTENSIONS = ['.jpg', '.gif'];
|
|
9
|
+
/**
|
|
10
|
+
* The provider rejects anything heavier, so an oversized attachment fails the whole send.
|
|
11
|
+
* Media-library sizes are not always known, so the forms warn on it instead of blocking.
|
|
12
|
+
*/
|
|
13
|
+
export const MMS_MAX_FILE_SIZE_BYTES = 200 * 1024;
|
|
14
|
+
/**
|
|
15
|
+
* The provider downloads the attachment itself, so it has to be a public https url whose
|
|
16
|
+
* *path* ends with a supported image extension. Checking the path (not the whole url) matches
|
|
17
|
+
* the backend and keeps query strings — signatures, cache busters — from passing as extensions.
|
|
18
|
+
*
|
|
19
|
+
* PNG, JPEG, WebP and the legacy media-library files stored without an extension drop out
|
|
20
|
+
* here: all of them are fetchable, none is decoded by the provider.
|
|
21
|
+
*/
|
|
22
|
+
export const isMmsAttachmentUrl = url => {
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = new URL(url);
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
if (parsed.protocol !== 'https:' || !parsed.host) return false;
|
|
30
|
+
const path = parsed.pathname.toLowerCase();
|
|
31
|
+
return MMS_ALLOWED_EXTENSIONS.some(extension => path.endsWith(extension));
|
|
32
|
+
};
|
|
33
|
+
/** Whether the file is over the provider's size limit and will be rejected on send. */
|
|
34
|
+
export const isMmsAttachmentOversized = file => file.fileSize > MMS_MAX_FILE_SIZE_BYTES;
|
|
35
|
+
/**
|
|
36
|
+
* Subject length budget: the provider counts non-ASCII subjects against a much shorter
|
|
37
|
+
* limit, so a single non-ASCII character drops the whole subject to 13.
|
|
38
|
+
*/
|
|
39
|
+
export const getMmsSubjectMaxLength = subject =>
|
|
40
|
+
// eslint-disable-next-line no-control-regex
|
|
41
|
+
/^[\x00-\x7F]*$/.test(subject) ? MMS_SUBJECT_MAX_ASCII_LENGTH : MMS_SUBJECT_MAX_UNICODE_LENGTH;
|
|
42
|
+
/** Subject length in characters, counting astral symbols (emoji) as one. */
|
|
43
|
+
export const getMmsSubjectLength = subject => [...subject].length;
|
|
44
|
+
export const isMmsSubjectTooLong = subject => getMmsSubjectLength(subject) > getMmsSubjectMaxLength(subject);
|
|
45
|
+
/** Whether message_at points at one of the attachments, as the backend requires. */
|
|
46
|
+
export const isMmsMessageAtValid = (messageAt, attachmentsCount) => Number.isInteger(messageAt) && messageAt >= 0 && messageAt < attachmentsCount;
|
package/package.json
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@pushwoosh/kit-helpers": "^4.12.0",
|
|
30
30
|
"@pushwoosh/kit-icons": "^2.1.22",
|
|
31
31
|
"@pushwoosh/kit-typography": "^1.7.6",
|
|
32
|
-
"@pushwoosh/rpc-v2-http-api-data": "^0.2.
|
|
32
|
+
"@pushwoosh/rpc-v2-http-api-data": "^0.2.105",
|
|
33
33
|
"@pushwoosh/spa-context": "^1.7.1",
|
|
34
34
|
"idb-keyval": "^6.2.2",
|
|
35
35
|
"react-hook-form": "^7.68.0",
|
|
@@ -52,5 +52,5 @@
|
|
|
52
52
|
"react": "^16.14.0 || ^18.0.0",
|
|
53
53
|
"react-dom": "^16.14.0 || ^18.0.0"
|
|
54
54
|
},
|
|
55
|
-
"version": "1.3.
|
|
55
|
+
"version": "1.3.7"
|
|
56
56
|
}
|