cozy-client 60.30.6 → 60.31.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/dist/CozyClient.js +1 -1
- package/dist/models/__fixtures__/io.cozy.banners.json +1152 -0
- package/dist/models/banner.js +978 -0
- package/dist/models/banner.spec.js +761 -0
- package/dist/models/index.js +5 -1
- package/package.json +2 -2
- package/types/models/banner.d.ts +193 -0
- package/types/models/index.d.ts +2 -1
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
|
|
5
|
+
Object.defineProperty(exports, "__esModule", {
|
|
6
|
+
value: true
|
|
7
|
+
});
|
|
8
|
+
exports.dismiss = exports.getActiveBanner = exports.getActiveBanners = exports.getVisibleBanners = exports.getCta = exports.getSurface = exports.getSeverity = exports.byPriority = exports.isInWindow = exports.isDismissed = exports.isSupported = exports.isTrusted = exports.buildBannersQuery = exports.BANNERS_QUERY_NAME = exports.KNOWN_SURFACES = exports.KNOWN_SEVERITIES = exports.SUPPORTED_DOCTYPE_VERSION = exports.BANNERS_DOCTYPE = void 0;
|
|
9
|
+
|
|
10
|
+
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
|
|
11
|
+
|
|
12
|
+
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
|
|
13
|
+
|
|
14
|
+
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
|
|
15
|
+
|
|
16
|
+
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
17
|
+
|
|
18
|
+
var _isValid = _interopRequireDefault(require("date-fns/isValid"));
|
|
19
|
+
|
|
20
|
+
var _parseISO = _interopRequireDefault(require("date-fns/parseISO"));
|
|
21
|
+
|
|
22
|
+
var _dsl = require("../queries/dsl");
|
|
23
|
+
|
|
24
|
+
var _utils = require("./utils");
|
|
25
|
+
|
|
26
|
+
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
|
|
27
|
+
|
|
28
|
+
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
|
|
29
|
+
|
|
30
|
+
function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
|
|
31
|
+
|
|
32
|
+
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
33
|
+
|
|
34
|
+
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
|
|
35
|
+
|
|
36
|
+
var BANNERS_DOCTYPE = 'io.cozy.banners';
|
|
37
|
+
/**
|
|
38
|
+
* The version of the io.cozy.banners shape this code was written against.
|
|
39
|
+
* A document declaring a higher cozyMetadata.doctypeVersion is skipped.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
exports.BANNERS_DOCTYPE = BANNERS_DOCTYPE;
|
|
43
|
+
var SUPPORTED_DOCTYPE_VERSION = 1;
|
|
44
|
+
/**
|
|
45
|
+
* The severities the doctype defines. A value outside this list is rendered
|
|
46
|
+
* with DEFAULT_SEVERITY. Frozen because the fallbacks read it at call time, so
|
|
47
|
+
* a consumer mutating it would change what every other consumer renders.
|
|
48
|
+
*
|
|
49
|
+
* @type {readonly BannerSeverity[]}
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
exports.SUPPORTED_DOCTYPE_VERSION = SUPPORTED_DOCTYPE_VERSION;
|
|
53
|
+
var KNOWN_SEVERITIES = ['info', 'warning', 'error'];
|
|
54
|
+
exports.KNOWN_SEVERITIES = KNOWN_SEVERITIES;
|
|
55
|
+
Object.freeze(KNOWN_SEVERITIES);
|
|
56
|
+
/**
|
|
57
|
+
* The surfaces the doctype defines. A value outside this list is rendered on
|
|
58
|
+
* DEFAULT_SURFACE. Frozen for the same reason as KNOWN_SEVERITIES.
|
|
59
|
+
*
|
|
60
|
+
* @type {readonly BannerSurface[]}
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
var KNOWN_SURFACES = ['banner', 'modal'];
|
|
64
|
+
exports.KNOWN_SURFACES = KNOWN_SURFACES;
|
|
65
|
+
Object.freeze(KNOWN_SURFACES);
|
|
66
|
+
/** @type {BannerSeverity} */
|
|
67
|
+
|
|
68
|
+
var DEFAULT_SEVERITY = 'warning';
|
|
69
|
+
/** @type {BannerSurface} */
|
|
70
|
+
|
|
71
|
+
var DEFAULT_SURFACE = 'banner';
|
|
72
|
+
var STACK_APP = 'stack';
|
|
73
|
+
/**
|
|
74
|
+
* @typedef {object} BannerCta
|
|
75
|
+
* @property {string} label - The label, plain text, in the same language as the banner text
|
|
76
|
+
* @property {string} url - Absolute https URL the label points to
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @typedef {'quota'|'billing'|'trial'|'account'|'system'} BannerCategory
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @typedef {'info'|'warning'|'error'} BannerSeverity
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @typedef {'banner'|'modal'} BannerSurface
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @typedef {object} VisibleBannersOptions
|
|
93
|
+
* @property {Date} [now] - The current time, defaults to now
|
|
94
|
+
* @property {number} [supportedDoctypeVersion] - The highest version this client handles
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @typedef {import("../CozyClient").default} BannerClient A CozyClient instance
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @typedef {object} DismissOptions
|
|
103
|
+
* @property {string} [dismissedAt] - The timestamp to record, defaults to now
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @typedef {object} Banner An io.cozy.banners document
|
|
108
|
+
* @property {string} [_id] - Identifier of the document, minted by CouchDB
|
|
109
|
+
* @property {string} [_rev] - Revision identifier of the document
|
|
110
|
+
* @property {string} bannerId - Identifier of the banner itself
|
|
111
|
+
* @property {BannerCategory} category - What the banner is about
|
|
112
|
+
* @property {BannerSeverity} severity - info, warning or error
|
|
113
|
+
* @property {BannerSurface} surface - banner or modal
|
|
114
|
+
* @property {string} text - The message, in the language given by lang
|
|
115
|
+
* @property {string} lang - BCP 47 tag of the language text is written in
|
|
116
|
+
* @property {BannerCta|null} [cta] - Optional call to action
|
|
117
|
+
* @property {boolean} dismissible - Whether the client offers a dismiss control
|
|
118
|
+
* @property {string|null} [dismissedAt] - When the user dismissed this banner
|
|
119
|
+
* @property {number} priority - Sort order, highest first
|
|
120
|
+
* @property {string} startsAt - Start of the validity window, inclusive
|
|
121
|
+
* @property {string|null} [endsAt] - End of the validity window, exclusive
|
|
122
|
+
* @property {object} [source] - What produced the document
|
|
123
|
+
* @property {object} [cozyMetadata] - Document lifecycle metadata
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The name the banners query is stored under. It is namespaced rather than the
|
|
128
|
+
* bare doctype: `client.query` keys the store entry on this name but keys the
|
|
129
|
+
* in-flight promise on the definition, so an unrelated consumer naming its own
|
|
130
|
+
* `io.cozy.banners` query would make this one resolve nothing while that query
|
|
131
|
+
* is loading.
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
var BANNERS_QUERY_NAME = "".concat(BANNERS_DOCTYPE, "/all");
|
|
135
|
+
/**
|
|
136
|
+
* Query for the banners of the current instance.
|
|
137
|
+
*
|
|
138
|
+
* The query is named so every consumer shares one store entry rather than
|
|
139
|
+
* minting a new one per call, and the limit is lifted because the whole
|
|
140
|
+
* collection is read in full.
|
|
141
|
+
*
|
|
142
|
+
* @returns {import('../types').Query} A named query
|
|
143
|
+
*/
|
|
144
|
+
|
|
145
|
+
exports.BANNERS_QUERY_NAME = BANNERS_QUERY_NAME;
|
|
146
|
+
|
|
147
|
+
var buildBannersQuery = function buildBannersQuery() {
|
|
148
|
+
return {
|
|
149
|
+
definition: (0, _dsl.Q)(BANNERS_DOCTYPE).UNSAFE_noLimit(),
|
|
150
|
+
options: {
|
|
151
|
+
as: BANNERS_QUERY_NAME
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Documents are only trusted when the stack wrote them. Permissions scope by
|
|
157
|
+
* doctype and verb, never by field, so any application allowed to record a
|
|
158
|
+
* dismissal is also able to author a document that looks like a platform
|
|
159
|
+
* message.
|
|
160
|
+
*
|
|
161
|
+
* This is not a security boundary, and should not be read as one. The stack
|
|
162
|
+
* does not own this field: cozy-client keeps whatever `cozyMetadata` the caller
|
|
163
|
+
* passes (CozyClient.js, ensureCozyMetadata spreads it last), so an application
|
|
164
|
+
* can set `createdByApp: 'stack'` on a document it authored itself and pass
|
|
165
|
+
* this check. It catches an application writing under its own name, which is
|
|
166
|
+
* the ordinary mistake, not one deliberately impersonating the stack. Enforcing
|
|
167
|
+
* that needs the doctype to be stack write only on the server side.
|
|
168
|
+
*
|
|
169
|
+
* It also only speaks to who created the document. An application holding write
|
|
170
|
+
* permission can rewrite the fields of a stack authored one, which the platform
|
|
171
|
+
* cannot prevent, so a client escapes text and never interprets markup in it.
|
|
172
|
+
*
|
|
173
|
+
* @param {Banner} banner - The document to check
|
|
174
|
+
* @returns {boolean} True when the stack is the author
|
|
175
|
+
*/
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
exports.buildBannersQuery = buildBannersQuery;
|
|
179
|
+
|
|
180
|
+
var isTrusted = function isTrusted(banner) {
|
|
181
|
+
return (0, _utils.getCreatedByApp)(banner) === STACK_APP;
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* cozy-stack writes numbers as strings in places, `cozyMetadata` declares
|
|
185
|
+
* `doctypeVersion` that way platform wide, so a value is parsed rather than
|
|
186
|
+
* compared as it comes. `Number()` alone is not enough: it maps null, "", false
|
|
187
|
+
* and [] to a finite 0, which would read as a real version or a real priority
|
|
188
|
+
* rather than as an unusable value, so only a number or a non blank string is
|
|
189
|
+
* parsed at all.
|
|
190
|
+
*
|
|
191
|
+
* @param {any} value - The value to read
|
|
192
|
+
* @param {number} fallback - What an absent or unusable value means
|
|
193
|
+
* @returns {number} The parsed number, or the fallback
|
|
194
|
+
*/
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
exports.isTrusted = isTrusted;
|
|
198
|
+
|
|
199
|
+
var toNumber = function toNumber(value, fallback) {
|
|
200
|
+
var parsed = typeof value === 'number' || typeof value === 'string' && value.trim() !== '' ? Number(value) : NaN;
|
|
201
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* @param {Banner} banner - The document to read
|
|
205
|
+
* @returns {number} The version, defaulting to 1 for a missing or unusable one
|
|
206
|
+
*/
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
var doctypeVersion = function doctypeVersion(banner) {
|
|
210
|
+
var _banner$cozyMetadata;
|
|
211
|
+
|
|
212
|
+
return toNumber(banner === null || banner === void 0 ? void 0 : (_banner$cozyMetadata = banner.cozyMetadata) === null || _banner$cozyMetadata === void 0 ? void 0 : _banner$cozyMetadata.doctypeVersion, 1);
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* @param {Banner} banner - The document to check
|
|
216
|
+
* @param {number} [supportedVersion] - The highest version this client handles
|
|
217
|
+
* @returns {boolean} True when the shape is one this client can read
|
|
218
|
+
*/
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
var isSupported = function isSupported(banner) {
|
|
222
|
+
var supportedVersion = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : SUPPORTED_DOCTYPE_VERSION;
|
|
223
|
+
return doctypeVersion(banner) <= supportedVersion;
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* A null value and an absent key mean the same thing for dismissedAt and
|
|
227
|
+
* endsAt, and `new Date(null)` is the Unix epoch rather than "no date", so
|
|
228
|
+
* every date is null checked before being parsed.
|
|
229
|
+
*
|
|
230
|
+
* Returns a timestamp rather than a Date so the comparisons stay primitive,
|
|
231
|
+
* and NaN when the value is present but unparseable, which the callers reject
|
|
232
|
+
* rather than silently treating as "no bound".
|
|
233
|
+
*
|
|
234
|
+
* This is the only date parser in the file. `Date.parse` is deliberately not
|
|
235
|
+
* used anywhere: it accepts implementation defined formats that `parseISO`
|
|
236
|
+
* rejects, and the two disagree by the local UTC offset on a date only value,
|
|
237
|
+
* so mixing them would give one document two different instants.
|
|
238
|
+
*
|
|
239
|
+
* @param {string|null|undefined} value - An ISO 8601 timestamp
|
|
240
|
+
* @returns {number|null} The timestamp, NaN when malformed, null when absent
|
|
241
|
+
*/
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
exports.isSupported = isSupported;
|
|
245
|
+
|
|
246
|
+
var toTime = function toTime(value) {
|
|
247
|
+
if (value === null || value === undefined) return null;
|
|
248
|
+
var parsed = (0, _parseISO.default)(value);
|
|
249
|
+
return (0, _isValid.default)(parsed) ? parsed.getTime() : NaN;
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* @param {Banner} banner - The document to check
|
|
253
|
+
* @returns {boolean} True when the user dismissed it
|
|
254
|
+
*/
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
var isDismissed = function isDismissed(banner) {
|
|
258
|
+
return (banner === null || banner === void 0 ? void 0 : banner.dismissedAt) != null;
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* The window is inclusive on startsAt and exclusive on endsAt, and an absent
|
|
262
|
+
* endsAt is open ended. A malformed bound hides the banner: a document whose
|
|
263
|
+
* window cannot be read is not one a client can decide to show.
|
|
264
|
+
*
|
|
265
|
+
* An absent startsAt hides it too. The doctype makes cta, dismissedAt and
|
|
266
|
+
* endsAt the only fields that may be missing, so a document with no lower bound
|
|
267
|
+
* is not open ended, it is unreadable. Treating it as "no bound" would make the
|
|
268
|
+
* missing field more permissive than a malformed one, and would let anything
|
|
269
|
+
* able to rewrite the document turn a scheduled banner into a permanent one by
|
|
270
|
+
* deleting both bounds.
|
|
271
|
+
*
|
|
272
|
+
* @param {Banner} banner - The document to check
|
|
273
|
+
* @param {Date} [now] - The current time
|
|
274
|
+
* @returns {boolean} True when the banner applies at that time
|
|
275
|
+
*/
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
exports.isDismissed = isDismissed;
|
|
279
|
+
|
|
280
|
+
var isInWindow = function isInWindow(banner) {
|
|
281
|
+
var now = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : new Date();
|
|
282
|
+
var startsAt = toTime(banner === null || banner === void 0 ? void 0 : banner.startsAt);
|
|
283
|
+
var endsAt = toTime(banner === null || banner === void 0 ? void 0 : banner.endsAt);
|
|
284
|
+
if (startsAt === null || Number.isNaN(startsAt)) return false;
|
|
285
|
+
if (Number.isNaN(endsAt)) return false;
|
|
286
|
+
var at = now.getTime();
|
|
287
|
+
if (Number.isNaN(at)) return false;
|
|
288
|
+
if (at < startsAt) return false;
|
|
289
|
+
if (endsAt !== null && at >= endsAt) return false;
|
|
290
|
+
return true;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
exports.isInWindow = isInWindow;
|
|
294
|
+
|
|
295
|
+
var priorityOf = function priorityOf(banner) {
|
|
296
|
+
return toNumber(banner === null || banner === void 0 ? void 0 : banner.priority, 0);
|
|
297
|
+
};
|
|
298
|
+
/**
|
|
299
|
+
* Highest priority first, then bannerId ascending so every client orders
|
|
300
|
+
* identically when priorities are equal.
|
|
301
|
+
*
|
|
302
|
+
* The tie break compares code units rather than calling localeCompare, whose
|
|
303
|
+
* result depends on the runtime's locale and ICU build. A missing or non
|
|
304
|
+
* numeric priority sorts as 0 instead of producing NaN, which would make the
|
|
305
|
+
* comparator intransitive and the sort order engine defined.
|
|
306
|
+
*
|
|
307
|
+
* Documents that lost their bannerId fall back on _id, as the deduplication
|
|
308
|
+
* does, so two of them do not compare equal and land in whatever order the
|
|
309
|
+
* database returned them in.
|
|
310
|
+
*
|
|
311
|
+
* @param {Banner} a - A banner
|
|
312
|
+
* @param {Banner} b - Another banner
|
|
313
|
+
* @returns {number} The comparison result
|
|
314
|
+
*/
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
var byPriority = function byPriority(a, b) {
|
|
318
|
+
var _ref, _a$bannerId, _ref2, _b$bannerId;
|
|
319
|
+
|
|
320
|
+
var byRank = priorityOf(b) - priorityOf(a);
|
|
321
|
+
if (byRank !== 0) return byRank;
|
|
322
|
+
var idA = String((_ref = (_a$bannerId = a === null || a === void 0 ? void 0 : a.bannerId) !== null && _a$bannerId !== void 0 ? _a$bannerId : a === null || a === void 0 ? void 0 : a._id) !== null && _ref !== void 0 ? _ref : '');
|
|
323
|
+
var idB = String((_ref2 = (_b$bannerId = b === null || b === void 0 ? void 0 : b.bannerId) !== null && _b$bannerId !== void 0 ? _b$bannerId : b === null || b === void 0 ? void 0 : b._id) !== null && _ref2 !== void 0 ? _ref2 : '');
|
|
324
|
+
if (idA < idB) return -1;
|
|
325
|
+
if (idA > idB) return 1;
|
|
326
|
+
return 0;
|
|
327
|
+
};
|
|
328
|
+
/**
|
|
329
|
+
* @param {Banner} banner - The document to read
|
|
330
|
+
* @returns {BannerSeverity} The severity to render, falling back when unknown
|
|
331
|
+
*/
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
exports.byPriority = byPriority;
|
|
335
|
+
|
|
336
|
+
var getSeverity = function getSeverity(banner) {
|
|
337
|
+
return KNOWN_SEVERITIES.includes(banner === null || banner === void 0 ? void 0 : banner.severity) ? banner.severity : DEFAULT_SEVERITY;
|
|
338
|
+
};
|
|
339
|
+
/**
|
|
340
|
+
* @param {Banner} banner - The document to read
|
|
341
|
+
* @returns {BannerSurface} The surface to render on, falling back when unknown
|
|
342
|
+
*/
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
exports.getSeverity = getSeverity;
|
|
346
|
+
|
|
347
|
+
var getSurface = function getSurface(banner) {
|
|
348
|
+
return KNOWN_SURFACES.includes(banner === null || banner === void 0 ? void 0 : banner.surface) ? banner.surface : DEFAULT_SURFACE;
|
|
349
|
+
};
|
|
350
|
+
/**
|
|
351
|
+
* Whitespace plus the zero width characters `trim` leaves behind. A label made
|
|
352
|
+
* only of these renders as a button with nothing on it. They are stripped to
|
|
353
|
+
* decide whether the label is empty, never from the label itself, so a script
|
|
354
|
+
* that uses a zero width joiner between glyphs keeps it.
|
|
355
|
+
*/
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
exports.getSurface = getSurface;
|
|
359
|
+
var BLANK = /[\s\u200B-\u200D\uFEFF]/g;
|
|
360
|
+
/**
|
|
361
|
+
* A call to action is only rendered when it carries a label and points at
|
|
362
|
+
* https, so a locally authored document cannot turn into a platform styled
|
|
363
|
+
* link on any scheme, and no banner renders a button the user cannot read.
|
|
364
|
+
* The scheme is matched case insensitively since URL schemes are.
|
|
365
|
+
*
|
|
366
|
+
* @param {Banner} banner - The document to read
|
|
367
|
+
* @returns {BannerCta|null} The call to action, or null when there is none to show
|
|
368
|
+
*/
|
|
369
|
+
|
|
370
|
+
var getCta = function getCta(banner) {
|
|
371
|
+
var cta = banner === null || banner === void 0 ? void 0 : banner.cta;
|
|
372
|
+
if (!cta || typeof cta.url !== 'string') return null;
|
|
373
|
+
|
|
374
|
+
if (typeof cta.label !== 'string' || cta.label.replace(BLANK, '') === '') {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return isHttpsUrl(cta.url) ? cta : null;
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
381
|
+
* A prefix check accepts "https://" and "https:example.org", which do not point
|
|
382
|
+
* where they look like they point, so the value is parsed instead. The parser
|
|
383
|
+
* rejects an https URL with no host outright, so reaching the scheme comparison
|
|
384
|
+
* already means there is one.
|
|
385
|
+
*
|
|
386
|
+
* @param {string} raw - The candidate URL
|
|
387
|
+
* @returns {boolean} True when it is an absolute https URL
|
|
388
|
+
*/
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
exports.getCta = getCta;
|
|
392
|
+
|
|
393
|
+
var isHttpsUrl = function isHttpsUrl(raw) {
|
|
394
|
+
try {
|
|
395
|
+
return new URL(raw).protocol === 'https:';
|
|
396
|
+
} catch (error) {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
var updatedAtOf = function updatedAtOf(banner) {
|
|
402
|
+
var _banner$cozyMetadata2;
|
|
403
|
+
|
|
404
|
+
var at = toTime(banner === null || banner === void 0 ? void 0 : (_banner$cozyMetadata2 = banner.cozyMetadata) === null || _banner$cozyMetadata2 === void 0 ? void 0 : _banner$cozyMetadata2.updatedAt);
|
|
405
|
+
return Number.isFinite(at) ? at : 0;
|
|
406
|
+
};
|
|
407
|
+
/**
|
|
408
|
+
* Orders two documents carrying the same bannerId, most relevant first. The
|
|
409
|
+
* highest supported doctypeVersion wins; equal versions fall back on the most
|
|
410
|
+
* recently written document and then on _id, so the winner never depends on
|
|
411
|
+
* the order the rows came back in.
|
|
412
|
+
*
|
|
413
|
+
* @param {Banner} candidate - The document being considered
|
|
414
|
+
* @param {Banner} current - The document currently held
|
|
415
|
+
* @returns {boolean} True when candidate should replace current
|
|
416
|
+
*/
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
var supersedes = function supersedes(candidate, current) {
|
|
420
|
+
var _candidate$_id, _current$_id;
|
|
421
|
+
|
|
422
|
+
var byVersion = doctypeVersion(candidate) - doctypeVersion(current);
|
|
423
|
+
if (byVersion !== 0) return byVersion > 0;
|
|
424
|
+
var byUpdate = updatedAtOf(candidate) - updatedAtOf(current);
|
|
425
|
+
if (byUpdate !== 0) return byUpdate > 0;
|
|
426
|
+
return String((_candidate$_id = candidate === null || candidate === void 0 ? void 0 : candidate._id) !== null && _candidate$_id !== void 0 ? _candidate$_id : '') < String((_current$_id = current === null || current === void 0 ? void 0 : current._id) !== null && _current$_id !== void 0 ? _current$_id : '');
|
|
427
|
+
};
|
|
428
|
+
/**
|
|
429
|
+
* The identity two documents are grouped and dismissed under. bannerId and _id
|
|
430
|
+
* are kept in separate namespaces: sharing one would let a document whose _id
|
|
431
|
+
* happens to equal another document's bannerId dismiss or supersede it. A
|
|
432
|
+
* document carrying neither keys on itself, so unrelated ones do not collapse
|
|
433
|
+
* onto a single `undefined` key.
|
|
434
|
+
*
|
|
435
|
+
* @param {Banner} banner - The document to key
|
|
436
|
+
* @returns {string|Banner} The grouping key
|
|
437
|
+
*/
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
var groupKey = function groupKey(banner) {
|
|
441
|
+
if ((banner === null || banner === void 0 ? void 0 : banner.bannerId) != null) return "bannerId:".concat(banner.bannerId);
|
|
442
|
+
if ((banner === null || banner === void 0 ? void 0 : banner._id) != null) return "_id:".concat(banner._id);
|
|
443
|
+
return banner;
|
|
444
|
+
};
|
|
445
|
+
/**
|
|
446
|
+
* Keeps the highest supported version of each bannerId. Without this the
|
|
447
|
+
* documents materialized on both sides of a version bump render twice.
|
|
448
|
+
*
|
|
449
|
+
* @param {Banner[]} banners - The documents to reduce
|
|
450
|
+
* @returns {Banner[]} One document per bannerId
|
|
451
|
+
*/
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
var keepHighestVersion = function keepHighestVersion(banners) {
|
|
455
|
+
var best = new Map();
|
|
456
|
+
|
|
457
|
+
var _iterator = _createForOfIteratorHelper(banners),
|
|
458
|
+
_step;
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
462
|
+
var banner = _step.value;
|
|
463
|
+
var key = groupKey(banner);
|
|
464
|
+
var current = best.get(key);
|
|
465
|
+
|
|
466
|
+
if (current === undefined || supersedes(banner, current)) {
|
|
467
|
+
best.set(key, banner);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} catch (err) {
|
|
471
|
+
_iterator.e(err);
|
|
472
|
+
} finally {
|
|
473
|
+
_iterator.f();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return Array.from(best.values());
|
|
477
|
+
};
|
|
478
|
+
/**
|
|
479
|
+
* The banners a client displays, in the order it displays them.
|
|
480
|
+
*
|
|
481
|
+
* A dismissal applies to the bannerId rather than to the single document
|
|
482
|
+
* carrying it, so a banner dismissed against one version stays hidden once the
|
|
483
|
+
* client moves to another. Deduplication runs last, on the documents that are
|
|
484
|
+
* actually renderable, so a newer version sitting outside its validity window
|
|
485
|
+
* never suppresses the older one that is still live.
|
|
486
|
+
*
|
|
487
|
+
* @param {Banner[]} banners - The documents read from the doctype
|
|
488
|
+
* @param {VisibleBannersOptions} [options] - Options
|
|
489
|
+
* @returns {Banner[]} The visible banners, ordered
|
|
490
|
+
*/
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
var getVisibleBanners = function getVisibleBanners(banners) {
|
|
494
|
+
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
495
|
+
var now = options.now instanceof Date && !Number.isNaN(options.now.getTime()) ? options.now : new Date(); // Validated rather than defaulted with ??, which only guards null and
|
|
496
|
+
// undefined: a NaN reaching isSupported makes every comparison false and
|
|
497
|
+
// hides every banner with no error.
|
|
498
|
+
|
|
499
|
+
var supportedVersion = toNumber(options.supportedDoctypeVersion, SUPPORTED_DOCTYPE_VERSION); // Only documents the stack wrote are read at all, dismissals included: a
|
|
500
|
+
// dismissal on a document an application authored must not hide a platform
|
|
501
|
+
// message.
|
|
502
|
+
|
|
503
|
+
var trusted = (banners !== null && banners !== void 0 ? banners : []).filter(isTrusted); // Collected before the version filter: a dismissal recorded by a client on a
|
|
504
|
+
// newer version of the document still applies to a client that only reads the
|
|
505
|
+
// older one, which would otherwise render a banner the user has closed.
|
|
506
|
+
|
|
507
|
+
var dismissed = new Set(trusted.filter(isDismissed).map(groupKey));
|
|
508
|
+
return keepHighestVersion(trusted.filter(function (banner) {
|
|
509
|
+
return isSupported(banner, supportedVersion) && !dismissed.has(groupKey(banner)) && isInWindow(banner, now);
|
|
510
|
+
})).sort(byPriority).map(resolve);
|
|
511
|
+
};
|
|
512
|
+
/**
|
|
513
|
+
* Applies every fallback the doctype defines, so a caller renders what it is
|
|
514
|
+
* given instead of reaching for the helpers itself: an unknown severity or
|
|
515
|
+
* surface is already replaced, and a call to action that is not an absolute
|
|
516
|
+
* https URL is already dropped.
|
|
517
|
+
*
|
|
518
|
+
* The doctype guarantees that a non dismissible banner on the modal surface
|
|
519
|
+
* carries a call to action, so the user is never left without a way out.
|
|
520
|
+
* Dropping an unusable one would break that guarantee and paint a blocking
|
|
521
|
+
* dialog with no dismiss control and no button, so the banner falls back to the
|
|
522
|
+
* surface that is never blocking.
|
|
523
|
+
*
|
|
524
|
+
* The document is returned unchanged when no fallback applied. The store relies
|
|
525
|
+
* on referential equality, and copying every banner on every read would make a
|
|
526
|
+
* memoized renderer repaint on every poll tick. An absent cta is not left
|
|
527
|
+
* absent though: the resolved shape always carries the key, so a caller reads
|
|
528
|
+
* one value rather than checking for both null and undefined.
|
|
529
|
+
*
|
|
530
|
+
* @param {Banner} banner - A visible banner
|
|
531
|
+
* @returns {Banner} The same banner, ready to render
|
|
532
|
+
*/
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
exports.getVisibleBanners = getVisibleBanners;
|
|
536
|
+
|
|
537
|
+
var resolve = function resolve(banner) {
|
|
538
|
+
var severity = getSeverity(banner);
|
|
539
|
+
var cta = getCta(banner);
|
|
540
|
+
var surface = getSurface(banner) === 'modal' && cta === null && !(banner !== null && banner !== void 0 && banner.dismissible) ? DEFAULT_SURFACE : getSurface(banner);
|
|
541
|
+
|
|
542
|
+
if (severity === (banner === null || banner === void 0 ? void 0 : banner.severity) && surface === (banner === null || banner === void 0 ? void 0 : banner.surface) && cta === (banner === null || banner === void 0 ? void 0 : banner.cta)) {
|
|
543
|
+
return banner;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return _objectSpread(_objectSpread({}, banner), {}, {
|
|
547
|
+
severity: severity,
|
|
548
|
+
surface: surface,
|
|
549
|
+
cta: cta
|
|
550
|
+
});
|
|
551
|
+
};
|
|
552
|
+
/**
|
|
553
|
+
* A conflict reaches this code in two shapes: a FetchError from the stack,
|
|
554
|
+
* which carries the HTTP status, and a PouchDB conflict, which the pouch link
|
|
555
|
+
* rewraps into a plain Error that only keeps the message.
|
|
556
|
+
*
|
|
557
|
+
* @param {Error & {status?: number}} error - The error to classify
|
|
558
|
+
* @returns {boolean} True when the write lost a revision race
|
|
559
|
+
*/
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
var isConflict = function isConflict(error) {
|
|
563
|
+
var _error$message;
|
|
564
|
+
|
|
565
|
+
return (error === null || error === void 0 ? void 0 : error.status) === 409 || /Document update conflict/.test((_error$message = error === null || error === void 0 ? void 0 : error.message) !== null && _error$message !== void 0 ? _error$message : '');
|
|
566
|
+
};
|
|
567
|
+
/**
|
|
568
|
+
* `client.query` does not always resolve a response. It resolves undefined when
|
|
569
|
+
* the client was built with an `onError` callback, which swallows the rejection,
|
|
570
|
+
* and when a query named the same as this one is already in flight with a
|
|
571
|
+
* different definition. Both are indistinguishable from an empty collection
|
|
572
|
+
* once the response is read, so they are rejected here instead: reporting a
|
|
573
|
+
* failed read as "there is nothing" hides a banner the user should see, and
|
|
574
|
+
* reports a dismissal that never happened as done.
|
|
575
|
+
*
|
|
576
|
+
* @param {object} response - The response of a query
|
|
577
|
+
* @returns {object} The same response
|
|
578
|
+
* @throws {Error} When the query resolved nothing
|
|
579
|
+
*/
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
var requireResponse = function requireResponse(response) {
|
|
583
|
+
if (response === null || response === undefined) {
|
|
584
|
+
throw new Error("The ".concat(BANNERS_DOCTYPE, " query resolved nothing. Either the client swallowed the error through its onError option, or another query is registered under the name ").concat(BANNERS_QUERY_NAME, "."));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return response;
|
|
588
|
+
};
|
|
589
|
+
/**
|
|
590
|
+
* The links disagree on how a missing document comes back: the stack link
|
|
591
|
+
* resolves `{data: null}` while the pouch link resolves `{data: []}`, and an
|
|
592
|
+
* empty array is truthy. Both mean there is nothing to write to.
|
|
593
|
+
*
|
|
594
|
+
* @param {object} response - The response of a getById query
|
|
595
|
+
* @returns {Banner|null} The document, or null when there is none
|
|
596
|
+
*/
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
var readOne = function readOne(response) {
|
|
600
|
+
var _requireResponse;
|
|
601
|
+
|
|
602
|
+
var data = (_requireResponse = requireResponse(response)) === null || _requireResponse === void 0 ? void 0 : _requireResponse.data;
|
|
603
|
+
var doc = Array.isArray(data) ? data[0] : data;
|
|
604
|
+
return doc !== null && doc !== void 0 && doc._id ? doc : null;
|
|
605
|
+
};
|
|
606
|
+
/**
|
|
607
|
+
* The banners to display, in the order to display them. This is the entry
|
|
608
|
+
* point: it queries the doctype and applies every rule the contract defines,
|
|
609
|
+
* so a caller does not compose the helpers itself.
|
|
610
|
+
*
|
|
611
|
+
* The whole collection is read rather than filtered by a Mango selector. The
|
|
612
|
+
* validity window, the dismissal and the version filter would each need their
|
|
613
|
+
* own index, and the doctype keeps the collection small on purpose: the stack
|
|
614
|
+
* deletes a banner when its condition clears.
|
|
615
|
+
*
|
|
616
|
+
* @param {BannerClient} client - A CozyClient instance
|
|
617
|
+
* @param {VisibleBannersOptions} [options] - Options
|
|
618
|
+
* @returns {Promise<Banner[]>} The banners to display, ready to render
|
|
619
|
+
* @throws {Error} When the doctype cannot be read, a missing permission included
|
|
620
|
+
*/
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
var getActiveBanners = /*#__PURE__*/function () {
|
|
624
|
+
var _ref3 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(client) {
|
|
625
|
+
var _requireResponse2;
|
|
626
|
+
|
|
627
|
+
var options,
|
|
628
|
+
_buildBannersQuery,
|
|
629
|
+
definition,
|
|
630
|
+
queryOptions,
|
|
631
|
+
data,
|
|
632
|
+
_args = arguments;
|
|
633
|
+
|
|
634
|
+
return _regenerator.default.wrap(function _callee$(_context) {
|
|
635
|
+
while (1) {
|
|
636
|
+
switch (_context.prev = _context.next) {
|
|
637
|
+
case 0:
|
|
638
|
+
options = _args.length > 1 && _args[1] !== undefined ? _args[1] : {};
|
|
639
|
+
_buildBannersQuery = buildBannersQuery(), definition = _buildBannersQuery.definition, queryOptions = _buildBannersQuery.options;
|
|
640
|
+
_context.t1 = requireResponse;
|
|
641
|
+
_context.next = 5;
|
|
642
|
+
return client.query(definition, queryOptions);
|
|
643
|
+
|
|
644
|
+
case 5:
|
|
645
|
+
_context.t2 = _context.sent;
|
|
646
|
+
_context.t3 = _requireResponse2 = (0, _context.t1)(_context.t2);
|
|
647
|
+
_context.t0 = _context.t3 === null;
|
|
648
|
+
|
|
649
|
+
if (_context.t0) {
|
|
650
|
+
_context.next = 10;
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
_context.t0 = _requireResponse2 === void 0;
|
|
655
|
+
|
|
656
|
+
case 10:
|
|
657
|
+
if (!_context.t0) {
|
|
658
|
+
_context.next = 14;
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
_context.t4 = void 0;
|
|
663
|
+
_context.next = 15;
|
|
664
|
+
break;
|
|
665
|
+
|
|
666
|
+
case 14:
|
|
667
|
+
_context.t4 = _requireResponse2.data;
|
|
668
|
+
|
|
669
|
+
case 15:
|
|
670
|
+
data = _context.t4;
|
|
671
|
+
return _context.abrupt("return", getVisibleBanners(Array.isArray(data) ? data : [], options));
|
|
672
|
+
|
|
673
|
+
case 17:
|
|
674
|
+
case "end":
|
|
675
|
+
return _context.stop();
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}, _callee);
|
|
679
|
+
}));
|
|
680
|
+
|
|
681
|
+
return function getActiveBanners(_x) {
|
|
682
|
+
return _ref3.apply(this, arguments);
|
|
683
|
+
};
|
|
684
|
+
}();
|
|
685
|
+
/**
|
|
686
|
+
* The single banner to display, for a surface that shows one at a time.
|
|
687
|
+
*
|
|
688
|
+
* @param {BannerClient} client - A CozyClient instance
|
|
689
|
+
* @param {VisibleBannersOptions} [options] - Options
|
|
690
|
+
* @returns {Promise<Banner|null>} The highest priority banner, or null
|
|
691
|
+
* @throws {Error} When the doctype cannot be read, a missing permission included
|
|
692
|
+
*/
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
exports.getActiveBanners = getActiveBanners;
|
|
696
|
+
|
|
697
|
+
var getActiveBanner = /*#__PURE__*/function () {
|
|
698
|
+
var _ref4 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(client) {
|
|
699
|
+
var options,
|
|
700
|
+
_yield$getActiveBanne,
|
|
701
|
+
_yield$getActiveBanne2,
|
|
702
|
+
first,
|
|
703
|
+
_args2 = arguments;
|
|
704
|
+
|
|
705
|
+
return _regenerator.default.wrap(function _callee2$(_context2) {
|
|
706
|
+
while (1) {
|
|
707
|
+
switch (_context2.prev = _context2.next) {
|
|
708
|
+
case 0:
|
|
709
|
+
options = _args2.length > 1 && _args2[1] !== undefined ? _args2[1] : {};
|
|
710
|
+
_context2.next = 3;
|
|
711
|
+
return getActiveBanners(client, options);
|
|
712
|
+
|
|
713
|
+
case 3:
|
|
714
|
+
_yield$getActiveBanne = _context2.sent;
|
|
715
|
+
_yield$getActiveBanne2 = (0, _slicedToArray2.default)(_yield$getActiveBanne, 1);
|
|
716
|
+
first = _yield$getActiveBanne2[0];
|
|
717
|
+
return _context2.abrupt("return", first !== null && first !== void 0 ? first : null);
|
|
718
|
+
|
|
719
|
+
case 7:
|
|
720
|
+
case "end":
|
|
721
|
+
return _context2.stop();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}, _callee2);
|
|
725
|
+
}));
|
|
726
|
+
|
|
727
|
+
return function getActiveBanner(_x2) {
|
|
728
|
+
return _ref4.apply(this, arguments);
|
|
729
|
+
};
|
|
730
|
+
}();
|
|
731
|
+
/**
|
|
732
|
+
* Records a dismissal. The field is idempotent, so a conflict is resolved by
|
|
733
|
+
* reading the document again and writing the value on the fresh revision.
|
|
734
|
+
*
|
|
735
|
+
* The dismissal is written to the document it is given. During a doctypeVersion
|
|
736
|
+
* migration the stack materializes the same bannerId under several versions,
|
|
737
|
+
* and the sibling documents are not reached from here.
|
|
738
|
+
*
|
|
739
|
+
* @param {BannerClient} client - A CozyClient instance
|
|
740
|
+
* @param {Banner & {_id: string}} banner - The banner to dismiss, as stored
|
|
741
|
+
* @param {DismissOptions} [options] - Options
|
|
742
|
+
* @returns {Promise<{data: Banner|null}>} The dismissed document, null when it is gone
|
|
743
|
+
*/
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
exports.getActiveBanner = getActiveBanner;
|
|
747
|
+
|
|
748
|
+
var dismiss = /*#__PURE__*/function () {
|
|
749
|
+
var _ref5 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3(client, banner) {
|
|
750
|
+
var _options$dismissedAt;
|
|
751
|
+
|
|
752
|
+
var options,
|
|
753
|
+
dismissedAt,
|
|
754
|
+
id,
|
|
755
|
+
siblings,
|
|
756
|
+
result,
|
|
757
|
+
_iterator2,
|
|
758
|
+
_step2,
|
|
759
|
+
sibling,
|
|
760
|
+
written,
|
|
761
|
+
_args3 = arguments;
|
|
762
|
+
|
|
763
|
+
return _regenerator.default.wrap(function _callee3$(_context3) {
|
|
764
|
+
while (1) {
|
|
765
|
+
switch (_context3.prev = _context3.next) {
|
|
766
|
+
case 0:
|
|
767
|
+
options = _args3.length > 2 && _args3[2] !== undefined ? _args3[2] : {};
|
|
768
|
+
dismissedAt = (_options$dismissedAt = options.dismissedAt) !== null && _options$dismissedAt !== void 0 ? _options$dismissedAt : new Date().toISOString();
|
|
769
|
+
id = banner === null || banner === void 0 ? void 0 : banner._id;
|
|
770
|
+
|
|
771
|
+
if (id) {
|
|
772
|
+
_context3.next = 5;
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
throw new Error('A banner needs an _id to be dismissed');
|
|
777
|
+
|
|
778
|
+
case 5:
|
|
779
|
+
_context3.next = 7;
|
|
780
|
+
return findSiblings(client, banner);
|
|
781
|
+
|
|
782
|
+
case 7:
|
|
783
|
+
siblings = _context3.sent;
|
|
784
|
+
result = {
|
|
785
|
+
data: null
|
|
786
|
+
};
|
|
787
|
+
_iterator2 = _createForOfIteratorHelper(siblings);
|
|
788
|
+
_context3.prev = 10;
|
|
789
|
+
|
|
790
|
+
_iterator2.s();
|
|
791
|
+
|
|
792
|
+
case 12:
|
|
793
|
+
if ((_step2 = _iterator2.n()).done) {
|
|
794
|
+
_context3.next = 20;
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
sibling = _step2.value;
|
|
799
|
+
_context3.next = 16;
|
|
800
|
+
return writeDismissal(client, sibling._id, dismissedAt);
|
|
801
|
+
|
|
802
|
+
case 16:
|
|
803
|
+
written = _context3.sent;
|
|
804
|
+
// The caller is answered with the document it asked about.
|
|
805
|
+
if (sibling._id === id) result = written;
|
|
806
|
+
|
|
807
|
+
case 18:
|
|
808
|
+
_context3.next = 12;
|
|
809
|
+
break;
|
|
810
|
+
|
|
811
|
+
case 20:
|
|
812
|
+
_context3.next = 25;
|
|
813
|
+
break;
|
|
814
|
+
|
|
815
|
+
case 22:
|
|
816
|
+
_context3.prev = 22;
|
|
817
|
+
_context3.t0 = _context3["catch"](10);
|
|
818
|
+
|
|
819
|
+
_iterator2.e(_context3.t0);
|
|
820
|
+
|
|
821
|
+
case 25:
|
|
822
|
+
_context3.prev = 25;
|
|
823
|
+
|
|
824
|
+
_iterator2.f();
|
|
825
|
+
|
|
826
|
+
return _context3.finish(25);
|
|
827
|
+
|
|
828
|
+
case 28:
|
|
829
|
+
return _context3.abrupt("return", result);
|
|
830
|
+
|
|
831
|
+
case 29:
|
|
832
|
+
case "end":
|
|
833
|
+
return _context3.stop();
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}, _callee3, null, [[10, 22, 25, 28]]);
|
|
837
|
+
}));
|
|
838
|
+
|
|
839
|
+
return function dismiss(_x3, _x4) {
|
|
840
|
+
return _ref5.apply(this, arguments);
|
|
841
|
+
};
|
|
842
|
+
}();
|
|
843
|
+
/**
|
|
844
|
+
* Every stored document sharing the banner's identity, the one the caller holds
|
|
845
|
+
* included. The whole collection is read because the versions of a bannerId are
|
|
846
|
+
* not reachable by id, and the doctype keeps the collection small on purpose.
|
|
847
|
+
*
|
|
848
|
+
* @param {BannerClient} client - A CozyClient instance
|
|
849
|
+
* @param {Banner} banner - The banner being dismissed
|
|
850
|
+
* @returns {Promise<Banner[]>} The documents to write the dismissal to
|
|
851
|
+
*/
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
exports.dismiss = dismiss;
|
|
855
|
+
|
|
856
|
+
var findSiblings = /*#__PURE__*/function () {
|
|
857
|
+
var _ref6 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee4(client, banner) {
|
|
858
|
+
var _buildBannersQuery2, definition, queryOptions, response, data, key, siblings;
|
|
859
|
+
|
|
860
|
+
return _regenerator.default.wrap(function _callee4$(_context4) {
|
|
861
|
+
while (1) {
|
|
862
|
+
switch (_context4.prev = _context4.next) {
|
|
863
|
+
case 0:
|
|
864
|
+
_buildBannersQuery2 = buildBannersQuery(), definition = _buildBannersQuery2.definition, queryOptions = _buildBannersQuery2.options;
|
|
865
|
+
_context4.t0 = requireResponse;
|
|
866
|
+
_context4.next = 4;
|
|
867
|
+
return client.query(definition, queryOptions);
|
|
868
|
+
|
|
869
|
+
case 4:
|
|
870
|
+
_context4.t1 = _context4.sent;
|
|
871
|
+
response = (0, _context4.t0)(_context4.t1);
|
|
872
|
+
data = Array.isArray(response.data) ? response.data : [];
|
|
873
|
+
key = groupKey(banner);
|
|
874
|
+
siblings = data.filter(function (doc) {
|
|
875
|
+
return (doc === null || doc === void 0 ? void 0 : doc._id) && groupKey(doc) === key;
|
|
876
|
+
}); // The document may have been deleted between the read and the click, in which
|
|
877
|
+
// case there is nothing left to write to.
|
|
878
|
+
|
|
879
|
+
return _context4.abrupt("return", siblings.length > 0 ? siblings : []);
|
|
880
|
+
|
|
881
|
+
case 10:
|
|
882
|
+
case "end":
|
|
883
|
+
return _context4.stop();
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}, _callee4);
|
|
887
|
+
}));
|
|
888
|
+
|
|
889
|
+
return function findSiblings(_x5, _x6) {
|
|
890
|
+
return _ref6.apply(this, arguments);
|
|
891
|
+
};
|
|
892
|
+
}();
|
|
893
|
+
/** How many times the write is attempted before a conflict reaches the caller. */
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
var DISMISS_ATTEMPTS = 2;
|
|
897
|
+
|
|
898
|
+
var writeDismissal = /*#__PURE__*/function () {
|
|
899
|
+
var _ref7 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee5(client, id, dismissedAt) {
|
|
900
|
+
var attempt, stored;
|
|
901
|
+
return _regenerator.default.wrap(function _callee5$(_context5) {
|
|
902
|
+
while (1) {
|
|
903
|
+
switch (_context5.prev = _context5.next) {
|
|
904
|
+
case 0:
|
|
905
|
+
attempt = 1;
|
|
906
|
+
|
|
907
|
+
case 1:
|
|
908
|
+
if (!(attempt <= DISMISS_ATTEMPTS)) {
|
|
909
|
+
_context5.next = 24;
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
_context5.t0 = readOne;
|
|
914
|
+
_context5.next = 5;
|
|
915
|
+
return client.query((0, _dsl.Q)(BANNERS_DOCTYPE).getById(id));
|
|
916
|
+
|
|
917
|
+
case 5:
|
|
918
|
+
_context5.t1 = _context5.sent;
|
|
919
|
+
stored = (0, _context5.t0)(_context5.t1);
|
|
920
|
+
|
|
921
|
+
if (stored) {
|
|
922
|
+
_context5.next = 9;
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return _context5.abrupt("return", {
|
|
927
|
+
data: null
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
case 9:
|
|
931
|
+
if (!isDismissed(stored)) {
|
|
932
|
+
_context5.next = 11;
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return _context5.abrupt("return", {
|
|
937
|
+
data: stored
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
case 11:
|
|
941
|
+
_context5.prev = 11;
|
|
942
|
+
_context5.next = 14;
|
|
943
|
+
return client.save(_objectSpread(_objectSpread({}, stored), {}, {
|
|
944
|
+
_type: BANNERS_DOCTYPE,
|
|
945
|
+
dismissedAt: dismissedAt
|
|
946
|
+
}));
|
|
947
|
+
|
|
948
|
+
case 14:
|
|
949
|
+
return _context5.abrupt("return", _context5.sent);
|
|
950
|
+
|
|
951
|
+
case 17:
|
|
952
|
+
_context5.prev = 17;
|
|
953
|
+
_context5.t2 = _context5["catch"](11);
|
|
954
|
+
|
|
955
|
+
if (!(!isConflict(_context5.t2) || attempt === DISMISS_ATTEMPTS)) {
|
|
956
|
+
_context5.next = 21;
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
throw _context5.t2;
|
|
961
|
+
|
|
962
|
+
case 21:
|
|
963
|
+
attempt++;
|
|
964
|
+
_context5.next = 1;
|
|
965
|
+
break;
|
|
966
|
+
|
|
967
|
+
case 24:
|
|
968
|
+
case "end":
|
|
969
|
+
return _context5.stop();
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}, _callee5, null, [[11, 17]]);
|
|
973
|
+
}));
|
|
974
|
+
|
|
975
|
+
return function writeDismissal(_x7, _x8, _x9) {
|
|
976
|
+
return _ref7.apply(this, arguments);
|
|
977
|
+
};
|
|
978
|
+
}();
|